diff --git a/cars/cars.py b/cars/cars.py index d46fea9c..6514721d 100644 --- a/cars/cars.py +++ b/cars/cars.py @@ -110,9 +110,24 @@ def main_cli(args, dry_run=False): # noqa: C901 del config["output"]["out_dir"] config_json_dir = os.path.abspath(os.path.dirname(args.conf)) - pipeline_name = config.get( - "pipeline", "sensors_to_dense_dsm_no_merging" - ) + pipeline_name = config.get("pipeline", "default") + old_pipelines = [ + "sensors_to_dense_dsm", + "sensors_to_dense_dsm_no_merging", + "sensors_to_dense_depth_maps", + "sensors_to_dense_point_clouds", + "dense_point_clouds_to_dense_dsm", + "dense_point_clouds_to_dense_dsm_no_merging", + ] + if pipeline_name in old_pipelines: + warnings.warn( + f"The 'pipeline' value {pipeline_name} corresponds to" + " an old pipeline." + "Using the default CARS pipeline instead.", + FutureWarning, + stacklevel=2, + ) + pipeline_name = "default" # Logging configuration with args Loglevel loglevel = getattr(args, "loglevel", "PROGRESS").upper() diff --git a/cars/orchestrator/orchestrator.py b/cars/orchestrator/orchestrator.py index 91d9601d..3607a7df 100644 --- a/cars/orchestrator/orchestrator.py +++ b/cars/orchestrator/orchestrator.py @@ -526,6 +526,10 @@ def reset_registries(self): """ Reset registries """ + + # cleanup the current registry before replacing it, to save files + self.cars_ds_savers_registry.cleanup() + # reset registries # CarsDataset savers registry self.cars_ds_savers_registry = saver_registry.CarsDatasetsRegistrySaver( diff --git a/cars/pipelines/__init__.py b/cars/pipelines/__init__.py index 96d41761..67b3670e 100644 --- a/cars/pipelines/__init__.py +++ b/cars/pipelines/__init__.py @@ -23,6 +23,4 @@ """ # Imports needed in order to register pipeline for Pipeline factory -from . import depth_maps_to_dsm # noqa: F401 -from . import sensor_to_dense_dsm # noqa: F401 -from . import sensor_to_sparse_dsm # noqa: F401 +from . import default # noqa: F401 diff --git a/cars/pipelines/conf_pipeline/depth_maps_to_dsm.json b/cars/pipelines/conf_pipeline/depth_maps_to_dsm.json deleted file mode 100644 index 544b7b4d..00000000 --- a/cars/pipelines/conf_pipeline/depth_maps_to_dsm.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - -} \ No newline at end of file diff --git a/cars/pipelines/conf_pipeline/sensor_to_dense_dsm.json b/cars/pipelines/conf_pipeline/sensor_to_dense_dsm.json deleted file mode 100644 index c5ac64b2..00000000 --- a/cars/pipelines/conf_pipeline/sensor_to_dense_dsm.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - - -} \ No newline at end of file diff --git a/cars/pipelines/conf_pipeline/sensor_to_pc.json b/cars/pipelines/conf_pipeline/sensor_to_pc.json deleted file mode 100644 index ba18f57e..00000000 --- a/cars/pipelines/conf_pipeline/sensor_to_pc.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "applications": { - "triangulation": { - "method": "line_of_sight_intersection", - "save_intermediate_data": true - } - } -} \ No newline at end of file diff --git a/cars/pipelines/conf_pipeline/sensor_to_sparse_dsm.json b/cars/pipelines/conf_pipeline/sensor_to_sparse_dsm.json deleted file mode 100644 index 7a73a41b..00000000 --- a/cars/pipelines/conf_pipeline/sensor_to_sparse_dsm.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} \ No newline at end of file diff --git a/cars/pipelines/conf_pipeline/__init__.py b/cars/pipelines/default/__init__.py similarity index 85% rename from cars/pipelines/conf_pipeline/__init__.py rename to cars/pipelines/default/__init__.py index ee2279c6..2cdf49d9 100644 --- a/cars/pipelines/conf_pipeline/__init__.py +++ b/cars/pipelines/default/__init__.py @@ -19,5 +19,8 @@ # limitations under the License. # """ -CARS pipeline configuration init file +CARS default pipeline module init file """ + +# Cars imports +from cars.pipelines.default import default_pipeline # noqa: F401 diff --git a/cars/pipelines/default/default_pipeline.py b/cars/pipelines/default/default_pipeline.py new file mode 100644 index 00000000..95f160ff --- /dev/null +++ b/cars/pipelines/default/default_pipeline.py @@ -0,0 +1,2166 @@ +#!/usr/bin/env python +# coding: utf8 +# +# Copyright (c) 2020 Centre National d'Etudes Spatiales (CNES). +# +# This file is part of CARS +# (see https://github.com/CNES/cars). +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# attribute-defined-outside-init is disabled so that we can create and use +# attributes however we need, to stick to the "everything is attribute" logic +# introduced in issue#895 +# pylint: disable=too-many-lines,attribute-defined-outside-init +""" +CARS default pipeline class file +""" +# Standard imports +from __future__ import print_function + +import copy +import logging +import os + +import numpy as np + +# CARS imports +from cars import __version__ +from cars.applications.application import Application +from cars.applications.dem_generation import ( + dem_generation_constants as dem_gen_cst, +) +from cars.applications.dem_generation import dem_generation_tools +from cars.applications.grid_generation import grid_correction +from cars.applications.point_cloud_fusion import pc_tif_tools +from cars.applications.sparse_matching import ( + sparse_matching_tools as sparse_mtch_tools, +) +from cars.core import constants_disparity as cst_disp +from cars.core import preprocessing, roi_tools +from cars.core.geometry.abstract_geometry import AbstractGeometry +from cars.core.inputs import get_descriptions_bands +from cars.core.utils import safe_makedirs +from cars.data_structures import cars_dataset +from cars.orchestrator import orchestrator +from cars.orchestrator.cluster.log_wrapper import cars_profile +from cars.pipelines.parameters import advanced_parameters +from cars.pipelines.parameters import advanced_parameters_constants as adv_cst +from cars.pipelines.parameters import depth_map_inputs +from cars.pipelines.parameters import depth_map_inputs_constants as depth_cst +from cars.pipelines.parameters import output_constants as out_cst +from cars.pipelines.parameters import output_parameters, sensor_inputs +from cars.pipelines.parameters import sensor_inputs_constants as sens_cst +from cars.pipelines.pipeline import Pipeline +from cars.pipelines.pipeline_constants import ( + ADVANCED, + APPLICATIONS, + GEOMETRY_PLUGIN, + INPUTS, + ORCHESTRATOR, + OUTPUT, +) +from cars.pipelines.pipeline_template import PipelineTemplate + + +@Pipeline.register( + "default", +) +class DefaultPipeline(PipelineTemplate): + """ + DefaultPipeline + """ + + # pylint: disable=too-many-instance-attributes + + def __init__(self, conf, config_json_dir=None): + """ + Creates pipeline + + Directly creates class attributes: + used_conf + generate_terrain_products + debug_with_roi + save_output_dsm + save_output_depth_map + save_output__point_clouds + geom_plugin_without_dem_and_geoid + geom_plugin_with_dem_and_geoid + dem_generation_roi + + :param pipeline_name: name of the pipeline. + :type pipeline_name: str + :param cfg: configuration {'matching_cost_method': value} + :type cfg: dictionary + :param config_json_dir: path to dir containing json + :type config_json_dir: str + """ + + # Used conf + self.used_conf = {} + + # check global conf + self.check_global_schema(conf) + + # Check conf orchestrator + self.used_conf[ORCHESTRATOR] = self.check_orchestrator( + conf.get(ORCHESTRATOR, None) + ) + + # Check conf inputs + inputs = self.check_inputs( + conf[INPUTS], config_json_dir=config_json_dir + ) + self.used_conf[INPUTS] = inputs + + # Check advanced parameters + # TODO static method in the base class + advanced = advanced_parameters.check_advanced_parameters( + conf.get(ADVANCED, {}), check_epipolar_a_priori=True + ) + self.used_conf[ADVANCED] = advanced + + if sens_cst.SENSORS in self.used_conf[INPUTS]: + # Check geometry plugin and overwrite geomodel in conf inputs + ( + inputs, + self.used_conf[GEOMETRY_PLUGIN], + self.geom_plugin_without_dem_and_geoid, + self.geom_plugin_with_dem_and_geoid, + self.dem_generation_roi, + ) = sensor_inputs.check_geometry_plugin( + inputs, advanced, conf.get(GEOMETRY_PLUGIN, None) + ) + self.used_conf[INPUTS] = inputs + + elif depth_cst.DEPTH_MAPS in self.used_conf[INPUTS]: + + # if there's an initial elevation with + # point clouds as inputs, generate a plugin (used in dsm_filling) + + self.geom_plugin_with_dem_and_geoid = ( + depth_map_inputs.check_geometry_plugin( + inputs, conf.get(GEOMETRY_PLUGIN, None) + ) + ) + + # Get ROI + ( + self.input_roi_poly, + self.input_roi_epsg, + ) = roi_tools.generate_roi_poly_from_inputs( + self.used_conf[INPUTS][sens_cst.ROI] + ) + + self.debug_with_roi = self.used_conf[ADVANCED][adv_cst.DEBUG_WITH_ROI] + + # Check conf output + output = self.check_output(conf[OUTPUT]) + self.used_conf[OUTPUT] = output + + prod_level = output[out_cst.PRODUCT_LEVEL] + + self.save_output_dsm = "dsm" in prod_level + self.save_output_depth_map = "depth_map" in prod_level + self.save_output_point_cloud = "point_cloud" in prod_level + + self.output_level_none = not ( + self.save_output_dsm + or self.save_output_depth_map + or self.save_output_point_cloud + ) + self.sensors_in_inputs = sens_cst.SENSORS in self.used_conf[INPUTS] + self.depth_maps_in_inputs = ( + depth_cst.DEPTH_MAPS in self.used_conf[INPUTS] + ) + self.merging = self.used_conf[ADVANCED][adv_cst.MERGING] + + # if point_cloud is in product_level, we need to guarantee that + # point clouds are in the output, by activating merging + self.merging = self.merging or self.save_output_point_cloud + + self.compute_depth_map = self.sensors_in_inputs and ( + not self.output_level_none + ) + + if self.output_level_none: + self.infer_conditions_from_applications(conf) + + self.save_all_intermediate_data = self.used_conf[ADVANCED][ + adv_cst.SAVE_INTERMEDIATE_DATA + ] + self.save_all_point_clouds_by_pair = self.used_conf[OUTPUT].get( + out_cst.SAVE_BY_PAIR, False + ) + + # Check conf application + application_conf = self.check_applications(conf.get(APPLICATIONS, {})) + + if self.sensors_in_inputs: + # Check conf application vs inputs application + application_conf = self.check_applications_with_inputs( + self.used_conf[INPUTS], application_conf + ) + + self.used_conf[APPLICATIONS] = application_conf + + self.config_full_res = copy.deepcopy(self.used_conf) + self.config_full_res.__delitem__("applications") + self.config_full_res[ADVANCED][adv_cst.EPIPOLAR_A_PRIORI] = {} + self.config_full_res[ADVANCED][adv_cst.TERRAIN_A_PRIORI] = {} + self.config_full_res[ADVANCED][adv_cst.USE_EPIPOLAR_A_PRIORI] = True + + def quit_on_app(self, app_name): + """ + Returns whether the pipeline should end after + the application was called. + + Only works if the output_level is empty, so that + the control is instead given to + """ + + if not self.output_level_none: + # custom quit step was not set, never quit early + return False + + return self.app_values[app_name] >= self.last_application_to_run + + def infer_conditions_from_applications(self, conf): + """ + Fills the condition booleans used later in the pipeline by going + through the applications and infering which application we should + end the pipeline on. + """ + + self.last_application_to_run = 0 + + sensor_to_depth_apps = { + "grid_generation": 1, # and 6 + "resampling": 2, # and 7 + "holes_detection": 3, + "sparse_matching": 4, + "dem_generation": 5, + "dense_matching": 8, + "dense_matches_filling.1": 9, + "dense_matches_filling.2": 10, + "triangulation": 11, + } + + depth_merge_apps = { + "point_cloud_fusion": 12, + "point_cloud_outliers_removing.1": 13, + "point_cloud_outliers_removing.2": 14, + } + + depth_to_dsm_apps = { + "pc_denoising": 15, + "point_cloud_rasterization": 16, + } + + self.app_values = {} + self.app_values.update(sensor_to_depth_apps) + self.app_values.update(depth_merge_apps) + self.app_values.update(depth_to_dsm_apps) + + app_conf = conf.get(APPLICATIONS, {}) + for key in app_conf: + + if adv_cst.SAVE_INTERMEDIATE_DATA not in app_conf[key]: + continue + + if not app_conf[key][adv_cst.SAVE_INTERMEDIATE_DATA]: + continue + + if key in sensor_to_depth_apps: + + if not self.sensors_in_inputs: + warn_msg = ( + "The application {} can only be used when sensor " + "images are given as an input. " + "Its configuration will be ignored." + ).format(key) + logging.warning(warn_msg) + + else: + self.compute_depth_map = True + self.last_application_to_run = max( + self.last_application_to_run, self.app_values[key] + ) + + elif key in depth_to_dsm_apps: + + if not (self.sensors_in_inputs or self.depth_maps_in_inputs): + warn_msg = ( + "The application {} can only be used when sensor " + "images or depth maps are given as an input. " + "Its configuration will be ignored." + ).format(key) + logging.warning(warn_msg) + + else: + if self.sensors_in_inputs: + self.compute_depth_map = True + + # enabled to start the depth map to dsm process + self.save_output_dsm = True + + self.last_application_to_run = max( + self.last_application_to_run, self.app_values[key] + ) + + elif key in depth_merge_apps: + + if not self.merging: + warn_msg = ( + "The application {} can only be used when merging " + "is activated (this parameter is located in the " + "'advanced' config key). " + "The application's configuration will be ignored." + ).format(key) + logging.warning(warn_msg) + + elif not (self.sensors_in_inputs or self.depth_maps_in_inputs): + warn_msg = ( + "The application {} can only be used when sensor " + "images or depth maps are given as an input. " + "Its configuration will be ignored." + ).format(key) + logging.warning(warn_msg) + + else: + if self.sensors_in_inputs: + self.compute_depth_map = True + + # enabled to start the depth map to dsm process + self.save_output_point_cloud = True + + self.last_application_to_run = max( + self.last_application_to_run, self.app_values[key] + ) + else: + warn_msg = ( + "The application {} was not recognized. Its configuration" + "will be ignored." + ).format(key) + logging.warning(warn_msg) + + if not (self.compute_depth_map or self.save_output_dsm): + log_msg = ( + "No product level was given. CARS has not detected any " + "data you wish to save. No computation will be done." + ) + logging.info(log_msg) + else: + log_msg = ( + "No product level was given. CARS has detected that you " + + "wish to run up to the {} application.".format( + next( + k + for k, v in self.app_values.items() + if v == self.last_application_to_run + ) + ) + ) + logging.warning(log_msg) + + @staticmethod + def check_inputs(conf, config_json_dir=None): + """ + Check the inputs given + + :param conf: configuration of inputs + :type conf: dict + :param config_json_dir: directory of used json, if + user filled paths with relative paths + :type config_json_dir: str + + :return: overloaded inputs + :rtype: dict + """ + if sens_cst.SENSORS in conf: + return sensor_inputs.sensors_check_inputs( + conf, config_json_dir=config_json_dir + ) + + return depth_map_inputs.check_depth_maps_inputs( + conf, config_json_dir=config_json_dir + ) + + @staticmethod + def check_output(conf): + """ + Check the output given + + :param conf: configuration of output + :type conf: dict + + :return overloader output + :rtype : dict + """ + return output_parameters.check_output_parameters(conf) + + def check_applications( + self, + conf, + ): + """ + Check the given configuration for applications, + and generates needed applications for pipeline. + + :param conf: configuration of applications + :type conf: dict + """ + + # Check if all specified applications are used + # Application in terrain_application are note used in + # the sensors_to_dense_depth_maps pipeline + needed_applications = [] + + if self.sensors_in_inputs: + needed_applications += [ + "grid_generation", + "resampling", + "holes_detection", + "dense_matches_filling.1", + "dense_matches_filling.2", + "sparse_matching", + "dense_matching", + "triangulation", + "dem_generation", + ] + + if self.save_output_dsm or self.save_output_point_cloud: + + needed_applications += ["pc_denoising"] + + if self.save_output_dsm: + needed_applications += ["point_cloud_rasterization"] + + if self.merging: # we have to merge point clouds, add merging apps + needed_applications += [ + "point_cloud_fusion", + "point_cloud_outliers_removing.1", + "point_cloud_outliers_removing.2", + ] + + for app_key in conf.keys(): + if app_key not in needed_applications: + msg = ( + f"No {app_key} application used in the " + + "default Cars pipeline" + ) + logging.error(msg) + raise NameError(msg) + + # Initialize used config + used_conf = {} + for app_key in needed_applications: + used_conf[app_key] = conf.get(app_key, {}) + used_conf[app_key]["save_intermediate_data"] = ( + self.save_all_intermediate_data + or used_conf[app_key].get("save_intermediate_data", False) + ) + + for app_key in [ + "point_cloud_fusion", + "point_cloud_outliers_removing.1", + "point_cloud_outliers_removing.2", + "pc_denoising", + ]: + if app_key in needed_applications: + used_conf[app_key]["save_by_pair"] = used_conf[app_key].get( + "save_by_pair", self.save_all_point_clouds_by_pair + ) + + self.epipolar_grid_generation_application = None + self.resampling_application = None + self.holes_detection_app = None + self.dense_matches_filling_1 = None + self.dense_matches_filling_2 = None + self.sparse_mtch_app = None + self.dense_matching_app = None + self.triangulation_application = None + self.dem_generation_application = None + self.pc_denoising_application = None + self.pc_outliers_removing_1_app = None + self.pc_outliers_removing_2_app = None + self.rasterization_application = None + self.pc_fusion_application = None + + if self.sensors_in_inputs: + # Epipolar grid generation + self.epipolar_grid_generation_application = Application( + "grid_generation", cfg=used_conf.get("grid_generation", {}) + ) + used_conf["grid_generation"] = ( + self.epipolar_grid_generation_application.get_conf() + ) + + # image resampling + self.resampling_application = Application( + "resampling", cfg=used_conf.get("resampling", {}) + ) + used_conf["resampling"] = self.resampling_application.get_conf() + + # holes detection + self.holes_detection_app = Application( + "holes_detection", cfg=used_conf.get("holes_detection", {}) + ) + used_conf["holes_detection"] = self.holes_detection_app.get_conf() + + # disparity filling 1 plane + self.dense_matches_filling_1 = Application( + "dense_matches_filling", + cfg=used_conf.get( + "dense_matches_filling.1", + {"method": "plane"}, + ), + ) + used_conf["dense_matches_filling.1"] = ( + self.dense_matches_filling_1.get_conf() + ) + + # disparity filling 2 + self.dense_matches_filling_2 = Application( + "dense_matches_filling", + cfg=used_conf.get( + "dense_matches_filling.2", + {"method": "zero_padding"}, + ), + ) + used_conf["dense_matches_filling.2"] = ( + self.dense_matches_filling_2.get_conf() + ) + + # Sparse Matching + self.sparse_mtch_app = Application( + "sparse_matching", cfg=used_conf.get("sparse_matching", {}) + ) + used_conf["sparse_matching"] = self.sparse_mtch_app.get_conf() + + # Matching + generate_performance_map = ( + self.used_conf[OUTPUT] + .get(out_cst.AUXILIARY, {}) + .get(out_cst.AUX_PERFORMANCE_MAP, False) + ) + dense_matching_config = used_conf.get("dense_matching", {}) + if generate_performance_map is True: + dense_matching_config["generate_performance_map"] = True + self.dense_matching_app = Application( + "dense_matching", cfg=dense_matching_config + ) + used_conf["dense_matching"] = self.dense_matching_app.get_conf() + + # Triangulation + self.triangulation_application = Application( + "triangulation", cfg=used_conf.get("triangulation", {}) + ) + used_conf["triangulation"] = ( + self.triangulation_application.get_conf() + ) + + # MNT generation + self.dem_generation_application = Application( + "dem_generation", cfg=used_conf.get("dem_generation", {}) + ) + used_conf["dem_generation"] = ( + self.dem_generation_application.get_conf() + ) + + if self.save_output_dsm or self.save_output_point_cloud: + + # Point cloud denoising + self.pc_denoising_application = Application( + "pc_denoising", + cfg=used_conf.get("pc_denoising", {"method": "none"}), + ) + used_conf["pc_denoising"] = self.pc_denoising_application.get_conf() + + if self.save_output_dsm: + + # Rasterization + self.rasterization_application = Application( + "point_cloud_rasterization", + cfg=used_conf.get("point_cloud_rasterization", {}), + ) + used_conf["point_cloud_rasterization"] = ( + self.rasterization_application.get_conf() + ) + + if self.merging: + + # Points cloud fusion + self.pc_fusion_application = Application( + "point_cloud_fusion", + cfg=used_conf.get("point_cloud_fusion", {}), + ) + used_conf["point_cloud_fusion"] = ( + self.pc_fusion_application.get_conf() + ) + + # Points cloud outlier removing small components + self.pc_outliers_removing_1_app = Application( + "point_cloud_outliers_removing", + cfg=used_conf.get( + "point_cloud_outliers_removing.1", + {"method": "small_components"}, + ), + ) + used_conf["point_cloud_outliers_removing.1"] = ( + self.pc_outliers_removing_1_app.get_conf() + ) + + # Points cloud outlier removing statistical + self.pc_outliers_removing_2_app = Application( + "point_cloud_outliers_removing", + cfg=used_conf.get( + "point_cloud_outliers_removing.2", + {"method": "statistical"}, + ), + ) + used_conf["point_cloud_outliers_removing.2"] = ( + self.pc_outliers_removing_2_app.get_conf() + ) + + return used_conf + + def check_applications_with_inputs(self, inputs_conf, application_conf): + """ + Check for each application the input and output configuration + consistency + + :param inputs_conf: inputs checked configuration + :type inputs_conf: dict + :param application_conf: application checked configuration + :type application_conf: dict + """ + + initial_elevation = ( + inputs_conf[sens_cst.INITIAL_ELEVATION]["dem"] is not None + ) + if self.sparse_mtch_app.elevation_delta_lower_bound is None: + self.sparse_mtch_app.used_config["elevation_delta_lower_bound"] = ( + -100 if initial_elevation else -1000 + ) + self.sparse_mtch_app.elevation_delta_lower_bound = ( + self.sparse_mtch_app.used_config["elevation_delta_lower_bound"] + ) + if self.sparse_mtch_app.elevation_delta_upper_bound is None: + self.sparse_mtch_app.used_config["elevation_delta_upper_bound"] = ( + 1000 if initial_elevation else 9000 + ) + self.sparse_mtch_app.elevation_delta_upper_bound = ( + self.sparse_mtch_app.used_config["elevation_delta_upper_bound"] + ) + application_conf["sparse_matching"] = self.sparse_mtch_app.get_conf() + + # check classification application parameter compare + # to each sensors inputs classification list + for application_key in application_conf: + if "classification" in application_conf[application_key]: + for item in inputs_conf["sensors"]: + if "classification" in inputs_conf["sensors"][item].keys(): + if inputs_conf["sensors"][item]["classification"]: + descriptions = get_descriptions_bands( + inputs_conf["sensors"][item]["classification"] + ) + if application_conf[application_key][ + "classification" + ] and not set( + application_conf[application_key][ + "classification" + ] + ).issubset( + set(descriptions) + ): + raise RuntimeError( + "The {} bands description {} ".format( + inputs_conf["sensors"][item][ + "classification" + ], + list(descriptions), + ) + + "and the {} config are not ".format( + application_key + ) + + "consistent: {}".format( + application_conf[application_key][ + "classification" + ] + ) + ) + for key1, key2 in inputs_conf["pairing"]: + corr_cfg = self.dense_matching_app.loader.get_conf() + img_left = inputs_conf["sensors"][key1]["image"] + img_right = inputs_conf["sensors"][key2]["image"] + classif_left = None + classif_right = None + if "classification" in inputs_conf["sensors"][key1]: + classif_left = inputs_conf["sensors"][key1]["classification"] + if "classification" in inputs_conf["sensors"][key2]: + classif_right = inputs_conf["sensors"][key2]["classification"] + self.dense_matching_app.corr_config = ( + self.dense_matching_app.loader.check_conf( + corr_cfg, + img_left, + img_right, + classif_left, + classif_right, + ) + ) + + return application_conf + + def sensor_to_depth_maps(self): # noqa: C901 + """ + Creates the depth map from the sensor images given in the input, + by following the CARS pipeline's steps. + """ + # pylint:disable=too-many-return-statements + + inputs = self.used_conf[INPUTS] + output = self.used_conf[OUTPUT] + + # Initialize epsg for terrain tiles + self.epsg = output[out_cst.EPSG] + if self.epsg is not None: + # Compute roi polygon, in output EPSG + self.roi_poly = preprocessing.compute_roi_poly( + self.input_roi_poly, self.input_roi_epsg, self.epsg + ) + + self.resolution = output[out_cst.RESOLUTION] + + # List of terrain roi corresponding to each epipolar pair + # Used to generate final terrain roi + self.list_terrain_roi = [] + + # initialize lists of points + self.list_epipolar_points_cloud = [] + self.list_sensor_pairs = sensor_inputs.generate_inputs( + inputs, self.geom_plugin_without_dem_and_geoid + ) + logging.info( + "Received {} stereo pairs configurations".format( + len(self.list_sensor_pairs) + ) + ) + + # pairs is a dict used to store the CarsDataset of + # all pairs, easily retrievable with pair keys + self.pairs = {} + + # triangulated_matches_list is used to store triangulated matches + # used in dem generation + self.triangulated_matches_list = [] + + for ( + pair_key, + sensor_image_left, + sensor_image_right, + ) in self.list_sensor_pairs: + + # initialize pairs for current pair + self.pairs[pair_key] = {} + self.pairs[pair_key]["sensor_image_left"] = sensor_image_left + self.pairs[pair_key]["sensor_image_right"] = sensor_image_right + + # Run applications + + # Run grid generation + # We generate grids with dem if it is provided. + # If not provided, grid are generated without dem and a dem + # will be generated, to use later for a new grid generation** + altitude_delta_min = inputs.get(sens_cst.INITIAL_ELEVATION, {}).get( + sens_cst.ALTITUDE_DELTA_MIN, None + ) + altitude_delta_max = inputs.get(sens_cst.INITIAL_ELEVATION, {}).get( + sens_cst.ALTITUDE_DELTA_MAX, None + ) + + if inputs[sens_cst.INITIAL_ELEVATION][sens_cst.DEM_PATH] is None: + geom_plugin = self.geom_plugin_without_dem_and_geoid + + if None not in (altitude_delta_min, altitude_delta_max): + raise RuntimeError( + "Dem path is mandatory for " + "the use of altitude deltas" + ) + else: + geom_plugin = self.geom_plugin_with_dem_and_geoid + + # Generate rectification grids + ( + self.pairs[pair_key]["grid_left"], + self.pairs[pair_key]["grid_right"], + ) = self.epipolar_grid_generation_application.run( + self.pairs[pair_key]["sensor_image_left"], + self.pairs[pair_key]["sensor_image_right"], + geom_plugin, + orchestrator=self.cars_orchestrator, + pair_folder=os.path.join( + self.dump_dir, + "epipolar_grid_generation", + "initial", + pair_key, + ), + pair_key=pair_key, + ) + + if self.quit_on_app("grid_generation"): + continue # keep iterating over pairs, but don't go further + + # Run holes detection + # Get classif depending on which filling is used + # For now, 2 filling application can be used, and be configured + # with any order. the .1 will be performed before the .2 + self.pairs[pair_key]["holes_classif"] = [] + self.pairs[pair_key]["holes_poly_margin"] = 0 + add_classif = False + if self.dense_matches_filling_1.used_method == "plane": + self.pairs[pair_key][ + "holes_classif" + ] += self.dense_matches_filling_1.get_classif() + self.pairs[pair_key]["holes_poly_margin"] = max( + self.pairs[pair_key]["holes_poly_margin"], + self.dense_matches_filling_1.get_poly_margin(), + ) + add_classif = True + if self.dense_matches_filling_2.used_method == "plane": + self.pairs[pair_key][ + "holes_classif" + ] += self.dense_matches_filling_2.get_classif() + self.pairs[pair_key]["holes_poly_margin"] = max( + self.pairs[pair_key]["holes_poly_margin"], + self.dense_matches_filling_2.get_poly_margin(), + ) + add_classif = True + + self.pairs[pair_key]["holes_bbox_left"] = [] + self.pairs[pair_key]["holes_bbox_right"] = [] + + if self.used_conf[ADVANCED][ + adv_cst.USE_EPIPOLAR_A_PRIORI + ] is False or (len(self.pairs[pair_key]["holes_classif"]) > 0): + # Run resampling only if needed: + # no a priori or needs to detect holes + + # Run epipolar resampling + ( + self.pairs[pair_key]["epipolar_image_left"], + self.pairs[pair_key]["epipolar_image_right"], + ) = self.resampling_application.run( + self.pairs[pair_key]["sensor_image_left"], + self.pairs[pair_key]["sensor_image_right"], + self.pairs[pair_key]["grid_left"], + self.pairs[pair_key]["grid_right"], + orchestrator=self.cars_orchestrator, + pair_folder=os.path.join( + self.dump_dir, "resampling", "initial", pair_key + ), + pair_key=pair_key, + margins_fun=self.sparse_mtch_app.get_margins_fun(), + tile_width=None, + tile_height=None, + add_color=False, + add_classif=add_classif, + ) + + if self.quit_on_app("resampling"): + continue # keep iterating over pairs, but don't go further + + # Generate the holes polygons in epipolar images + # They are only generated if dense_matches_filling + # applications are used later + ( + self.pairs[pair_key]["holes_bbox_left"], + self.pairs[pair_key]["holes_bbox_right"], + ) = self.holes_detection_app.run( + self.pairs[pair_key]["epipolar_image_left"], + self.pairs[pair_key]["epipolar_image_right"], + classification=self.pairs[pair_key]["holes_classif"], + margin=self.pairs[pair_key]["holes_poly_margin"], + orchestrator=self.cars_orchestrator, + pair_folder=os.path.join( + self.dump_dir, "hole_detection", pair_key + ), + pair_key=pair_key, + ) + + if self.quit_on_app("holes_detection"): + continue # keep iterating over pairs, but don't go further + + if self.used_conf[ADVANCED][adv_cst.USE_EPIPOLAR_A_PRIORI] is False: + # Run epipolar sparse_matching application + ( + self.pairs[pair_key]["epipolar_matches_left"], + _, + ) = self.sparse_mtch_app.run( + self.pairs[pair_key]["epipolar_image_left"], + self.pairs[pair_key]["epipolar_image_right"], + self.pairs[pair_key]["grid_left"].attributes[ + "disp_to_alt_ratio" + ], + orchestrator=self.cars_orchestrator, + pair_folder=os.path.join( + self.dump_dir, "sparse_matching", pair_key + ), + pair_key=pair_key, + ) + + if self.quit_on_app("sparse_matching"): + continue # keep iterating over pairs, but don't go further + + # Run cluster breakpoint to compute sifts: force computation + self.cars_orchestrator.breakpoint() + + # Run grid correction application + save_corrected_grid = ( + self.epipolar_grid_generation_application.get_save_grids() + ) + if self.used_conf[ADVANCED][adv_cst.USE_EPIPOLAR_A_PRIORI] is False: + # Estimate grid correction if no epipolar a priori + # Filter and save matches + self.pairs[pair_key]["matches_array"] = ( + self.sparse_mtch_app.filter_matches( + self.pairs[pair_key]["epipolar_matches_left"], + self.pairs[pair_key]["grid_left"], + self.pairs[pair_key]["grid_right"], + orchestrator=self.cars_orchestrator, + pair_key=pair_key, + pair_folder=os.path.join( + self.dump_dir, "sparse_matching", pair_key + ), + save_matches=(self.sparse_mtch_app.get_save_matches()), + ) + ) + # Compute grid correction + save_matches = self.sparse_mtch_app.get_save_matches() + ( + self.pairs[pair_key]["grid_correction_coef"], + self.pairs[pair_key]["corrected_matches_array"], + self.pairs[pair_key]["corrected_matches_cars_ds"], + _, + _, + ) = grid_correction.estimate_right_grid_correction( + self.pairs[pair_key]["matches_array"], + self.pairs[pair_key]["grid_right"], + initial_cars_ds=self.pairs[pair_key][ + "epipolar_matches_left" + ], + save_matches=save_matches, + pair_folder=os.path.join( + self.dump_dir, "grid_correction", "initial", pair_key + ), + pair_key=pair_key, + orchestrator=self.cars_orchestrator, + ) + # Correct grid right + self.pairs[pair_key]["corrected_grid_right"] = ( + grid_correction.correct_grid( + self.pairs[pair_key]["grid_right"], + self.pairs[pair_key]["grid_correction_coef"], + save_corrected_grid, + os.path.join( + self.dump_dir, + "grid_correction", + "initial", + pair_key, + ), + ) + ) + + # Clean grid at the end of processing if required + if not (save_corrected_grid or save_matches): + self.cars_orchestrator.add_to_clean( + os.path.join( + self.dump_dir, + "grid_correction", + "initial", + pair_key, + ) + ) + + self.pairs[pair_key]["corrected_grid_left"] = self.pairs[ + pair_key + ]["grid_left"] + + # Triangulate matches + self.pairs[pair_key]["triangulated_matches"] = ( + dem_generation_tools.triangulate_sparse_matches( + self.pairs[pair_key]["sensor_image_left"], + self.pairs[pair_key]["sensor_image_right"], + self.pairs[pair_key]["grid_left"], + self.pairs[pair_key]["corrected_grid_right"], + self.pairs[pair_key]["corrected_matches_array"], + geom_plugin, + ) + ) + + # filter triangulated_matches + matches_filter_knn = ( + self.sparse_mtch_app.get_matches_filter_knn() + ) + matches_filter_dev_factor = ( + self.sparse_mtch_app.get_matches_filter_dev_factor() + ) + self.pairs[pair_key]["filtered_triangulated_matches"] = ( + sparse_mtch_tools.filter_point_cloud_matches( + self.pairs[pair_key]["triangulated_matches"], + matches_filter_knn=matches_filter_knn, + matches_filter_dev_factor=matches_filter_dev_factor, + ) + ) + + self.triangulated_matches_list.append( + self.pairs[pair_key]["filtered_triangulated_matches"] + ) + + # quit if any app in the loop over the pairs was the last one + if ( + self.quit_on_app("grid_generation") + or self.quit_on_app("resampling") + or self.quit_on_app("holes_detection") + or self.quit_on_app("sparse_matching") + ): + return True + + if self.used_conf[ADVANCED][adv_cst.USE_EPIPOLAR_A_PRIORI]: + # Use a priori + dem_median = self.used_conf[ADVANCED][adv_cst.TERRAIN_A_PRIORI][ + adv_cst.DEM_MEDIAN + ] + dem_min = self.used_conf[ADVANCED][adv_cst.TERRAIN_A_PRIORI][ + adv_cst.DEM_MIN + ] + dem_max = self.used_conf[ADVANCED][adv_cst.TERRAIN_A_PRIORI][ + adv_cst.DEM_MAX + ] + altitude_delta_min = self.used_conf[ADVANCED][ + adv_cst.TERRAIN_A_PRIORI + ][adv_cst.ALTITUDE_DELTA_MIN] + altitude_delta_max = self.used_conf[ADVANCED][ + adv_cst.TERRAIN_A_PRIORI + ][adv_cst.ALTITUDE_DELTA_MAX] + else: + dem_generation_output_dir = os.path.join( + self.dump_dir, "dem_generation" + ) + safe_makedirs(dem_generation_output_dir) + # Use initial elevation if provided, and generate dems + # Generate MNT from matches + dem = self.dem_generation_application.run( + self.triangulated_matches_list, + dem_generation_output_dir, + inputs[sens_cst.INITIAL_ELEVATION][sens_cst.GEOID], + dem_roi_to_use=self.dem_generation_roi, + ) + # Same geometry plugin if we use exogenous dem + # as initial elevation always used before if provided + dem_median = dem.attributes[dem_gen_cst.DEM_MEDIAN_PATH] + if ( + inputs[sens_cst.INITIAL_ELEVATION][sens_cst.DEM_PATH] + is not None + ): + dem_median = inputs[sens_cst.INITIAL_ELEVATION][ + sens_cst.DEM_PATH + ] + + if ( + dem_median + != inputs[sens_cst.INITIAL_ELEVATION][sens_cst.DEM_PATH] + ): + self.geom_plugin_with_dem_and_geoid = ( + sensor_inputs.generate_geometry_plugin_with_dem( + self.used_conf[GEOMETRY_PLUGIN], + inputs, + dem=dem_median, + ) + ) + dem_min = dem.attributes[dem_gen_cst.DEM_MIN_PATH] + dem_max = dem.attributes[dem_gen_cst.DEM_MAX_PATH] + + # update used configuration with terrain a priori + if None not in (altitude_delta_min, altitude_delta_max): + advanced_parameters.update_conf( + self.used_conf, + dem_median=dem_median, + altitude_delta_min=altitude_delta_min, + altitude_delta_max=altitude_delta_max, + ) + else: + advanced_parameters.update_conf( + self.used_conf, + dem_median=dem_median, + dem_min=dem_min, + dem_max=dem_max, + ) + + advanced_parameters.update_conf( + self.config_full_res, + dem_median=dem_median, + dem_min=dem_min, + dem_max=dem_max, + ) + + # quit only after the configuration was updated + if self.quit_on_app("dem_generation"): + return True + + # Define param + use_global_disp_range = self.dense_matching_app.use_global_disp_range + + if self.pc_denoising_application is not None: + denoising_overload_fun = ( + self.pc_denoising_application.get_triangulation_overload() + ) + else: + denoising_overload_fun = None + + self.pairs_names = [ + pair_name for pair_name, _, _ in self.list_sensor_pairs + ] + + for cloud_id, (pair_key, _, _) in enumerate(self.list_sensor_pairs): + # Geometry plugin with dem will be used for the grid generation + geom_plugin = self.geom_plugin_with_dem_and_geoid + if self.used_conf[ADVANCED][adv_cst.USE_EPIPOLAR_A_PRIORI] is False: + + if ( + inputs[sens_cst.INITIAL_ELEVATION][sens_cst.DEM_PATH] + is None + ): + # Generate grids with new MNT + ( + self.pairs[pair_key]["new_grid_left"], + self.pairs[pair_key]["new_grid_right"], + ) = self.epipolar_grid_generation_application.run( + self.pairs[pair_key]["sensor_image_left"], + self.pairs[pair_key]["sensor_image_right"], + geom_plugin, + orchestrator=self.cars_orchestrator, + pair_folder=os.path.join( + self.dump_dir, + "epipolar_grid_generation", + "new_mnt", + pair_key, + ), + pair_key=pair_key, + ) + + # Correct grids with former matches + # Transform matches to new grids + + new_grid_matches_array = ( + AbstractGeometry.transform_matches_from_grids( + self.pairs[pair_key]["corrected_matches_array"], + self.pairs[pair_key]["corrected_grid_left"], + self.pairs[pair_key]["corrected_grid_right"], + self.pairs[pair_key]["new_grid_left"], + self.pairs[pair_key]["new_grid_right"], + ) + ) + save_matches = self.sparse_mtch_app.get_save_matches() + # Estimate grid_correction + ( + self.pairs[pair_key]["grid_correction_coef"], + self.pairs[pair_key]["corrected_matches_array"], + self.pairs[pair_key]["corrected_matches_cars_ds"], + _, + _, + ) = grid_correction.estimate_right_grid_correction( + new_grid_matches_array, + self.pairs[pair_key]["new_grid_right"], + save_matches=save_matches, + initial_cars_ds=self.pairs[pair_key][ + "epipolar_matches_left" + ], + pair_folder=os.path.join( + self.dump_dir, "grid_correction", "new", pair_key + ), + pair_key=pair_key, + orchestrator=self.cars_orchestrator, + ) + + # Correct grid right + + self.pairs[pair_key]["corrected_grid_right"] = ( + grid_correction.correct_grid( + self.pairs[pair_key]["new_grid_right"], + self.pairs[pair_key]["grid_correction_coef"], + save_corrected_grid, + os.path.join( + self.dump_dir, + "grid_correction", + "new", + pair_key, + ), + ) + ) + + if not (save_corrected_grid or save_matches): + self.cars_orchestrator.add_to_clean( + os.path.join( + self.dump_dir, + "grid_correction", + "new", + pair_key, + ) + ) + + # Use the new grid as uncorrected grid + self.pairs[pair_key]["grid_right"] = self.pairs[pair_key][ + "new_grid_right" + ] + + self.pairs[pair_key]["corrected_grid_left"] = self.pairs[ + pair_key + ]["new_grid_left"] + + # matches filter params + matches_filter_knn = ( + self.sparse_mtch_app.get_matches_filter_knn() + ) + matches_filter_dev_factor = ( + self.sparse_mtch_app.get_matches_filter_dev_factor() + ) + if use_global_disp_range: + # Triangulate new matches + self.pairs[pair_key]["triangulated_matches"] = ( + dem_generation_tools.triangulate_sparse_matches( + self.pairs[pair_key]["sensor_image_left"], + self.pairs[pair_key]["sensor_image_right"], + self.pairs[pair_key]["corrected_grid_left"], + self.pairs[pair_key]["corrected_grid_right"], + self.pairs[pair_key]["corrected_matches_array"], + geometry_plugin=geom_plugin, + ) + ) + # filter triangulated_matches + # Filter outliers + self.pairs[pair_key]["filtered_triangulated_matches"] = ( + sparse_mtch_tools.filter_point_cloud_matches( + self.pairs[pair_key]["triangulated_matches"], + matches_filter_knn=matches_filter_knn, + matches_filter_dev_factor=matches_filter_dev_factor, + ) + ) + + # Compute disp_min and disp_max + ( + dmin, + dmax, + ) = sparse_mtch_tools.compute_disp_min_disp_max( + self.pairs[pair_key]["filtered_triangulated_matches"], + self.cars_orchestrator, + disp_margin=( + self.sparse_mtch_app.get_disparity_margin() + ), + pair_key=pair_key, + disp_to_alt_ratio=self.pairs[pair_key][ + "corrected_grid_left" + ].attributes["disp_to_alt_ratio"], + ) + + advanced_parameters.update_conf( + self.config_full_res, + dmin=dmin, + dmax=dmax, + pair_key=pair_key, + ) + else: + # Use epipolar a priori + # load the disparity range + [dmin, dmax] = self.used_conf[ADVANCED][ + adv_cst.EPIPOLAR_A_PRIORI + ][pair_key][adv_cst.DISPARITY_RANGE] + # load the grid correction coefficient + self.pairs[pair_key]["grid_correction_coef"] = self.used_conf[ + ADVANCED + ][adv_cst.EPIPOLAR_A_PRIORI][pair_key][adv_cst.GRID_CORRECTION] + self.pairs[pair_key]["corrected_grid_left"] = self.pairs[ + pair_key + ]["grid_left"] + # no correction if the grid correction coefs are None + if self.pairs[pair_key]["grid_correction_coef"] is None: + self.pairs[pair_key]["corrected_grid_right"] = self.pairs[ + pair_key + ]["grid_right"] + else: + # Correct grid right with provided epipolar a priori + self.pairs[pair_key]["corrected_grid_right"] = ( + grid_correction.correct_grid_from_1d( + self.pairs[pair_key]["grid_right"], + self.pairs[pair_key]["grid_correction_coef"], + save_corrected_grid, + os.path.join( + self.dump_dir, "grid_correction", pair_key + ), + ) + ) + advanced_parameters.update_conf( + self.config_full_res, + dmin=dmin, + dmax=dmax, + pair_key=pair_key, + ) + # Run epipolar resampling + + # Update used_conf configuration with epipolar a priori + # Add global min and max computed with grids + advanced_parameters.update_conf( + self.used_conf, + grid_correction_coef=self.pairs[pair_key][ + "grid_correction_coef" + ], + pair_key=pair_key, + ) + advanced_parameters.update_conf( + self.config_full_res, + grid_correction_coef=self.pairs[pair_key][ + "grid_correction_coef" + ], + pair_key=pair_key, + ) + # saved used configuration + cars_dataset.save_dict( + self.used_conf, + os.path.join(self.out_dir, "used_conf.json"), + safe_save=True, + ) + + # Generate min and max disp grids + # Global disparity min and max will be computed from + # these grids + dense_matching_pair_folder = os.path.join( + self.dump_dir, "dense_matching", pair_key + ) + if use_global_disp_range: + # Generate min and max disp grids from constants + # sensor image is not used here + # TODO remove when only local diparity range will be used + disp_range_grid = ( + self.dense_matching_app.generate_disparity_grids( + self.pairs[pair_key]["sensor_image_right"], + self.pairs[pair_key]["corrected_grid_right"], + self.geom_plugin_with_dem_and_geoid, + dmin=dmin, + dmax=dmax, + pair_folder=dense_matching_pair_folder, + ) + ) + elif None in (altitude_delta_min, altitude_delta_max): + # Generate min and max disp grids from dems + disp_range_grid = ( + self.dense_matching_app.generate_disparity_grids( + self.pairs[pair_key]["sensor_image_right"], + self.pairs[pair_key]["corrected_grid_right"], + self.geom_plugin_with_dem_and_geoid, + dem_min=dem_min, + dem_max=dem_max, + dem_median=dem_median, + pair_folder=dense_matching_pair_folder, + ) + ) + else: + # Generate min and max disp grids from deltas + disp_range_grid = ( + self.dense_matching_app.generate_disparity_grids( + self.pairs[pair_key]["sensor_image_right"], + self.pairs[pair_key]["corrected_grid_right"], + self.geom_plugin_with_dem_and_geoid, + altitude_delta_min=altitude_delta_min, + altitude_delta_max=altitude_delta_max, + dem_median=dem_median, + pair_folder=dense_matching_pair_folder, + ) + ) + + # Get margins used in dense matching, + dense_matching_margins_fun = ( + self.dense_matching_app.get_margins_fun( + self.pairs[pair_key]["corrected_grid_left"], disp_range_grid + ) + ) + + # TODO add in metadata.json max diff max - min + # Update used_conf configuration with epipolar a priori + # Add global min and max computed with grids + advanced_parameters.update_conf( + self.used_conf, + dmin=np.min( + disp_range_grid[0, 0]["disp_min_grid"].values + ), # TODO compute dmin dans dmax + dmax=np.max(disp_range_grid[0, 0]["disp_max_grid"].values), + pair_key=pair_key, + ) + advanced_parameters.update_conf( + self.config_full_res, + dmin=np.min( + disp_range_grid[0, 0]["disp_min_grid"].values + ), # TODO compute dmin dans dmax + dmax=np.max(disp_range_grid[0, 0]["disp_max_grid"].values), + pair_key=pair_key, + ) + + # saved used configuration + cars_dataset.save_dict( + self.used_conf, + os.path.join(self.out_dir, "used_conf.json"), + safe_save=True, + ) + + # Generate roi + epipolar_roi = preprocessing.compute_epipolar_roi( + self.input_roi_poly, + self.input_roi_epsg, + self.geom_plugin_with_dem_and_geoid, + self.pairs[pair_key]["sensor_image_left"], + self.pairs[pair_key]["sensor_image_right"], + self.pairs[pair_key]["corrected_grid_left"], + self.pairs[pair_key]["corrected_grid_right"], + os.path.join(self.dump_dir, "compute_epipolar_roi", pair_key), + disp_min=np.min( + disp_range_grid[0, 0]["disp_min_grid"].values + ), # TODO compute dmin dans dmax + disp_max=np.max(disp_range_grid[0, 0]["disp_max_grid"].values), + ) + + # Generate new epipolar images + # Generated with corrected grids + # Optimal size is computed for the worst case scenario + # found with epipolar disparity range grids + + ( + optimum_tile_size, + local_tile_optimal_size_fun, + ) = self.dense_matching_app.get_optimal_tile_size( + disp_range_grid, + self.cars_orchestrator.cluster.checked_conf_cluster[ + "max_ram_per_worker" + ], + ) + ( + new_epipolar_image_left, + new_epipolar_image_right, + ) = self.resampling_application.run( + self.pairs[pair_key]["sensor_image_left"], + self.pairs[pair_key]["sensor_image_right"], + self.pairs[pair_key]["corrected_grid_left"], + self.pairs[pair_key]["corrected_grid_right"], + orchestrator=self.cars_orchestrator, + pair_folder=os.path.join( + self.dump_dir, "resampling", "corrected_grid", pair_key + ), + pair_key=pair_key, + margins_fun=dense_matching_margins_fun, + tile_width=optimum_tile_size, + tile_height=optimum_tile_size, + add_color=True, + add_classif=True, + epipolar_roi=epipolar_roi, + ) + + # Run epipolar matching application + epipolar_disparity_map = self.dense_matching_app.run( + new_epipolar_image_left, + new_epipolar_image_right, + local_tile_optimal_size_fun, + orchestrator=self.cars_orchestrator, + pair_folder=os.path.join( + self.dump_dir, "dense_matching", pair_key + ), + pair_key=pair_key, + disp_range_grid=disp_range_grid, + compute_disparity_masks=False, + disp_to_alt_ratio=self.pairs[pair_key][ + "corrected_grid_left" + ].attributes["disp_to_alt_ratio"], + ) + + if self.quit_on_app("dense_matching"): + continue # keep iterating over pairs, but don't go further + + # Dense matches filling + if self.dense_matches_filling_1.used_method == "plane": + # Fill holes in disparity map + (filled_with_1_epipolar_disparity_map) = ( + self.dense_matches_filling_1.run( + epipolar_disparity_map, + self.pairs[pair_key]["holes_bbox_left"], + self.pairs[pair_key]["holes_bbox_right"], + disp_min=np.min( + disp_range_grid[0, 0]["disp_min_grid"].values + ), + disp_max=np.max( + disp_range_grid[0, 0]["disp_max_grid"].values + ), + orchestrator=self.cars_orchestrator, + pair_folder=os.path.join( + self.dump_dir, "dense_matches_filling_1", pair_key + ), + pair_key=pair_key, + ) + ) + else: + # Fill with zeros + (filled_with_1_epipolar_disparity_map) = ( + self.dense_matches_filling_1.run( + epipolar_disparity_map, + orchestrator=self.cars_orchestrator, + pair_folder=os.path.join( + self.dump_dir, "dense_matches_filling_1", pair_key + ), + pair_key=pair_key, + ) + ) + + if self.quit_on_app("dense_matches_filling.1"): + continue # keep iterating over pairs, but don't go further + + if self.dense_matches_filling_2.used_method == "plane": + # Fill holes in disparity map + (filled_with_2_epipolar_disparity_map) = ( + self.dense_matches_filling_2.run( + filled_with_1_epipolar_disparity_map, + self.pairs[pair_key]["holes_bbox_left"], + self.pairs[pair_key]["holes_bbox_right"], + disp_min=np.min( + disp_range_grid[0, 0]["disp_min_grid"].values + ), + disp_max=np.max( + disp_range_grid[0, 0]["disp_max_grid"].values + ), + orchestrator=self.cars_orchestrator, + pair_folder=os.path.join( + self.dump_dir, "dense_matches_filling_2", pair_key + ), + pair_key=pair_key, + ) + ) + else: + # Fill with zeros + (filled_with_2_epipolar_disparity_map) = ( + self.dense_matches_filling_2.run( + filled_with_1_epipolar_disparity_map, + orchestrator=self.cars_orchestrator, + pair_folder=os.path.join( + self.dump_dir, "dense_matches_filling_2", pair_key + ), + pair_key=pair_key, + ) + ) + + if self.quit_on_app("dense_matches_filling.2"): + continue # keep iterating over pairs, but don't go further + + if self.epsg is None: + # compute epsg + # Epsg uses global disparity min and max + self.epsg = preprocessing.compute_epsg( + self.pairs[pair_key]["sensor_image_left"], + self.pairs[pair_key]["sensor_image_right"], + self.pairs[pair_key]["corrected_grid_left"], + self.pairs[pair_key]["corrected_grid_right"], + self.geom_plugin_with_dem_and_geoid, + disp_min=np.min( + disp_range_grid[0, 0]["disp_min_grid"].values + ), + disp_max=np.max( + disp_range_grid[0, 0]["disp_max_grid"].values + ), + ) + # Compute roi polygon, in input EPSG + self.roi_poly = preprocessing.compute_roi_poly( + self.input_roi_poly, self.input_roi_epsg, self.epsg + ) + + # Checking disparity intervals indicators + if self.used_conf[APPLICATIONS]["dense_matching"][ + "generate_confidence_intervals" + ]: + intervals = [cst_disp.INTERVAL_INF, cst_disp.INTERVAL_SUP] + intervals_pair_flag = False + for key, item in self.dense_matching_app.corr_config[ + "pipeline" + ].items(): + if ( + cst_disp.CONFIDENCE_KEY in key + and item["confidence_method"] == cst_disp.INTERVAL + ): + indicator = key.split(".") + if not intervals_pair_flag: + if len(indicator) > 1: + intervals[0] += "." + indicator[-1] + intervals[1] += "." + indicator[-1] + # Only processing the first encountered interval + intervals_pair_flag = True + else: + warn_msg = ( + "Multiple confidence intervals " + "is not supported. {} will be " + "ignored. Only {} will be processed" + ).format(key, intervals) + logging.warning(warn_msg) + else: + intervals = None + + if isinstance(output[sens_cst.GEOID], str): + output_geoid_path = output[sens_cst.GEOID] + elif ( + isinstance(output[sens_cst.GEOID], bool) + and output[sens_cst.GEOID] + ): + package_path = os.path.dirname(__file__) + output_geoid_path = os.path.join( + package_path, + "..", + "..", + "conf", + sensor_inputs.CARS_GEOID_PATH, + ) + else: + # default case : stay on the ellipsoid + output_geoid_path = None + + depth_map_dir = None + if self.save_output_depth_map: + depth_map_dir = os.path.join( + self.out_dir, "depth_map", pair_key + ) + safe_makedirs(depth_map_dir) + + # Run epipolar triangulation application + epipolar_points_cloud = self.triangulation_application.run( + self.pairs[pair_key]["sensor_image_left"], + self.pairs[pair_key]["sensor_image_right"], + new_epipolar_image_left, + self.pairs[pair_key]["corrected_grid_left"], + self.pairs[pair_key]["corrected_grid_right"], + filled_with_2_epipolar_disparity_map, + self.epsg, + self.geom_plugin_without_dem_and_geoid, + denoising_overload_fun=denoising_overload_fun, + source_pc_names=self.pairs_names, + orchestrator=self.cars_orchestrator, + pair_dump_dir=os.path.join( + self.dump_dir, "triangulation", pair_key + ), + pair_key=pair_key, + uncorrected_grid_right=self.pairs[pair_key]["grid_right"], + geoid_path=output_geoid_path, + cloud_id=cloud_id, + intervals=intervals, + pair_output_dir=depth_map_dir, + save_output_color=bool(depth_map_dir) + and self.auxiliary[out_cst.AUX_COLOR], + save_output_classification=bool(depth_map_dir) + and self.auxiliary[out_cst.AUX_CLASSIFICATION], + save_output_filling=bool(depth_map_dir) + and self.auxiliary[out_cst.AUX_FILLING], + save_output_mask=bool(depth_map_dir) + and self.auxiliary[out_cst.AUX_MASK], + save_output_performance_map=bool(depth_map_dir) + and self.auxiliary[out_cst.AUX_PERFORMANCE_MAP], + ) + + if self.quit_on_app("triangulation"): + continue # keep iterating over pairs, but don't go further + + if self.merging: + self.list_epipolar_points_cloud.append(epipolar_points_cloud) + # denoising available only if we'll go further in the pipeline + elif self.save_output_dsm or self.save_output_point_cloud: + denoised_epipolar_points_cloud = ( + self.pc_denoising_application.run( + epipolar_points_cloud, + orchestrator=self.cars_orchestrator, + pair_folder=os.path.join( + self.dump_dir, "denoising", pair_key + ), + pair_key=pair_key, + ) + ) + + self.list_epipolar_points_cloud.append( + denoised_epipolar_points_cloud + ) + + if self.quit_on_app("pc_denoising"): + continue # keep iterating over pairs, but don't go further + + if self.save_output_dsm or self.save_output_point_cloud: + # Compute terrain bounding box /roi related to + # current images + (current_terrain_roi_bbox) = preprocessing.compute_terrain_bbox( + self.pairs[pair_key]["sensor_image_left"], + self.pairs[pair_key]["sensor_image_right"], + new_epipolar_image_left, + self.pairs[pair_key]["corrected_grid_left"], + self.pairs[pair_key]["corrected_grid_right"], + self.epsg, + self.geom_plugin_with_dem_and_geoid, + resolution=self.resolution, + disp_min=np.min( + disp_range_grid[0, 0]["disp_min_grid"].values + ), + disp_max=np.max( + disp_range_grid[0, 0]["disp_max_grid"].values + ), + roi_poly=(None if self.debug_with_roi else self.roi_poly), + orchestrator=self.cars_orchestrator, + pair_key=pair_key, + pair_folder=os.path.join( + self.dump_dir, "terrain_bbox", pair_key + ), + check_inputs=True, + ) + self.list_terrain_roi.append(current_terrain_roi_bbox) + + # compute terrain bounds for later use + ( + self.terrain_bounds, + self.optimal_terrain_tile_width, + ) = preprocessing.compute_terrain_bounds( + self.list_terrain_roi, + roi_poly=(None if self.debug_with_roi else self.roi_poly), + resolution=self.resolution, + ) + + # quit if any app in the loop over the pairs was the last one + if ( + self.quit_on_app("dense_matching") + or self.quit_on_app("dense_matches_filling.1") + or self.quit_on_app("dense_matches_filling.2") + or self.quit_on_app("triangulation") + or self.quit_on_app("pc_denoising") + ): + return True + + cars_dataset.save_dict( + self.config_full_res, + os.path.join(self.out_dir, "refined_config_dense_dsm.json"), + safe_save=True, + ) + + return False + + def rasterize_point_cloud(self): + """ + Final step of the pipeline: rasterize the point + cloud created in the prior steps. + """ + + rasterization_dump_dir = os.path.join(self.dump_dir, "rasterization") + + dsm_file_name = ( + os.path.join( + self.out_dir, + out_cst.DSM_DIRECTORY, + "dsm.tif", + ) + if self.save_output_dsm + else None + ) + + color_file_name = ( + os.path.join( + self.out_dir, + out_cst.DSM_DIRECTORY, + "color.tif", + ) + if self.save_output_dsm + and self.used_conf[OUTPUT][out_cst.AUXILIARY][out_cst.AUX_COLOR] + else None + ) + + performance_map_file_name = ( + os.path.join( + self.out_dir, + out_cst.DSM_DIRECTORY, + "performance_map.tif", + ) + if self.save_output_dsm + and self.used_conf[OUTPUT][out_cst.AUXILIARY][ + out_cst.AUX_PERFORMANCE_MAP + ] + else None + ) + + classif_file_name = ( + os.path.join( + self.out_dir, + out_cst.DSM_DIRECTORY, + "classification.tif", + ) + if self.save_output_dsm + and self.used_conf[OUTPUT][out_cst.AUXILIARY][ + out_cst.AUX_CLASSIFICATION + ] + else None + ) + + mask_file_name = ( + os.path.join( + self.out_dir, + out_cst.DSM_DIRECTORY, + "mask.tif", + ) + if self.save_output_dsm + and self.used_conf[OUTPUT][out_cst.AUXILIARY][out_cst.AUX_MASK] + else None + ) + + contributing_pair_file_name = ( + os.path.join( + self.out_dir, + out_cst.DSM_DIRECTORY, + "contributing_pair.tif", + ) + if self.save_output_dsm + and self.used_conf[OUTPUT][out_cst.AUXILIARY][ + out_cst.AUX_CONTRIBUTING_PAIR + ] + else None + ) + + filling_file_name = ( + os.path.join( + self.out_dir, + out_cst.DSM_DIRECTORY, + "filling.tif", + ) + if self.save_output_dsm + and self.used_conf[OUTPUT][out_cst.AUXILIARY][out_cst.AUX_FILLING] + else None + ) + + # rasterize point cloud + _ = self.rasterization_application.run( + self.point_cloud_to_rasterize, + self.epsg, + resolution=self.resolution, + orchestrator=self.cars_orchestrator, + dsm_file_name=dsm_file_name, + color_file_name=color_file_name, + classif_file_name=classif_file_name, + performance_map_file_name=performance_map_file_name, + mask_file_name=mask_file_name, + contributing_pair_file_name=contributing_pair_file_name, + filling_file_name=filling_file_name, + color_dtype=self.color_type, + dump_dir=rasterization_dump_dir, + ) + + # Cleaning: don't keep terrain bbox if save_intermediate_data + # is not activated + if not self.used_conf[ADVANCED][adv_cst.SAVE_INTERMEDIATE_DATA]: + self.cars_orchestrator.add_to_clean( + os.path.join(self.dump_dir, "terrain_bbox") + ) + + return self.quit_on_app("point_cloud_rasterization") + + def preprocess_depth_maps(self): + """ + Adds multiple processing steps to the depth maps : + Merging, filtering, denoising, outliers removing. + Creates the point cloud that will be rasterized in + the last step of the pipeline. + """ + + if not self.merging: + self.point_cloud_to_rasterize = ( + self.list_epipolar_points_cloud, + self.terrain_bounds, + ) + self.color_type = self.point_cloud_to_rasterize[0][ + 0 + ].attributes.get("color_type", None) + else: + # Merge point clouds + pc_outliers_removing_1_margins = ( + self.pc_outliers_removing_1_app.get_on_ground_margin( + resolution=self.resolution + ) + ) + pc_outliers_removing_2_margins = ( + self.pc_outliers_removing_2_app.get_on_ground_margin( + resolution=self.resolution + ) + ) + + # find which application produce the final version of the + # point cloud. The last generated point cloud will be saved + # as official point cloud product if save_output_point_cloud + # is True. + + last_pc_application = None + # denoising application will produce a point cloud, unless + # it uses the 'none' method. + if self.pc_denoising_application.used_method != "none": + last_pc_application = "denoising" + elif ( + self.pc_outliers_removing_2_app.used_config.get( + "activated", False + ) + is True + ): + last_pc_application = "pc_outliers_removing_2" + elif ( + self.pc_outliers_removing_1_app.used_config.get( + "activated", False + ) + is True + ): + last_pc_application = "pc_outliers_removing_1" + else: + last_pc_application = "fusion" + + raster_app_margin = 0 + if self.rasterization_application is not None: + raster_app_margin = self.rasterization_application.get_margins( + self.resolution + ) + + merged_points_clouds = self.pc_fusion_application.run( + self.list_epipolar_points_cloud, + self.terrain_bounds, + self.epsg, + source_pc_names=( + self.pairs_names if self.compute_depth_map else None + ), + orchestrator=self.cars_orchestrator, + margins=( + pc_outliers_removing_1_margins + + pc_outliers_removing_2_margins + + raster_app_margin + ), + optimal_terrain_tile_width=self.optimal_terrain_tile_width, + roi=(self.roi_poly if self.debug_with_roi else None), + save_laz_output=self.save_output_point_cloud + and last_pc_application == "fusion", + ) + + if self.quit_on_app("point_cloud_fusion"): + return True + + # Remove outliers with small components method + filtered_1_merged_points_clouds = ( + self.pc_outliers_removing_1_app.run( + merged_points_clouds, + orchestrator=self.cars_orchestrator, + save_laz_output=self.save_output_point_cloud + and last_pc_application == "pc_outliers_removing_1", + ) + ) + + if self.quit_on_app("point_cloud_outliers_removing.1"): + return True + + # Remove outliers with statistical components method + filtered_2_merged_points_clouds = ( + self.pc_outliers_removing_2_app.run( + filtered_1_merged_points_clouds, + orchestrator=self.cars_orchestrator, + save_laz_output=self.save_output_point_cloud + and last_pc_application == "pc_outliers_removing_2", + ) + ) + + if self.quit_on_app("point_cloud_outliers_removing.2"): + return True + + # denoise point cloud + denoised_merged_points_clouds = self.pc_denoising_application.run( + filtered_2_merged_points_clouds, + orchestrator=self.cars_orchestrator, + save_laz_output=self.save_output_point_cloud + and last_pc_application == "denoising", + ) + + if self.quit_on_app("pc_denoising"): + return True + + # Rasterize merged and filtered point cloud + self.point_cloud_to_rasterize = denoised_merged_points_clouds + + # try getting the color type from multiple sources + self.color_type = self.list_epipolar_points_cloud[0].attributes.get( + "color_type", + self.point_cloud_to_rasterize.attributes.get( + "color_type", None + ), + ) + + return False + + def load_input_depth_maps(self): + """ + Loads all the data and creates all the variables used + later when processing a depth map, as if it was just computed. + """ + + # get epsg + self.epsg = self.used_conf[OUTPUT][out_cst.EPSG] + + # compute epsg + epsg_cloud = pc_tif_tools.compute_epsg_from_point_cloud( + self.used_conf[INPUTS][depth_cst.DEPTH_MAPS] + ) + if self.epsg is None: + self.epsg = epsg_cloud + + self.resolution = self.used_conf[OUTPUT][out_cst.RESOLUTION] + + # Compute roi polygon, in input EPSG + self.roi_poly = preprocessing.compute_roi_poly( + self.input_roi_poly, self.input_roi_epsg, self.epsg + ) + + if not self.merging: + # compute bounds + self.terrain_bounds = pc_tif_tools.get_bounds( + self.used_conf[INPUTS][depth_cst.DEPTH_MAPS], + self.epsg, + roi_poly=self.roi_poly, + ) + + self.list_epipolar_points_cloud = ( + pc_tif_tools.generate_point_clouds( + self.used_conf[INPUTS][depth_cst.DEPTH_MAPS], + self.cars_orchestrator, + tile_size=1000, + ) + ) + else: + # Compute terrain bounds and transform point clouds + ( + self.terrain_bounds, + self.list_epipolar_points_cloud, + ) = pc_tif_tools.transform_input_pc( + self.used_conf[INPUTS][depth_cst.DEPTH_MAPS], + self.epsg, + roi_poly=self.roi_poly, + epipolar_tile_size=1000, # TODO change it + orchestrator=self.cars_orchestrator, + ) + + # Compute number of superposing point cloud for density + max_number_superposing_point_clouds = ( + pc_tif_tools.compute_max_nb_point_clouds( + self.list_epipolar_points_cloud + ) + ) + + # Compute average distance between two points + average_distance_point_cloud = ( + pc_tif_tools.compute_average_distance( + self.list_epipolar_points_cloud + ) + ) + self.optimal_terrain_tile_width = min( + self.pc_outliers_removing_1_app.get_optimal_tile_size( + self.cars_orchestrator.cluster.checked_conf_cluster[ + "max_ram_per_worker" + ], + superposing_point_clouds=( + max_number_superposing_point_clouds + ), + point_cloud_resolution=average_distance_point_cloud, + ), + self.pc_outliers_removing_2_app.get_optimal_tile_size( + self.cars_orchestrator.cluster.checked_conf_cluster[ + "max_ram_per_worker" + ], + superposing_point_clouds=( + max_number_superposing_point_clouds + ), + point_cloud_resolution=average_distance_point_cloud, + ), + self.rasterization_application.get_optimal_tile_size( + self.cars_orchestrator.cluster.checked_conf_cluster[ + "max_ram_per_worker" + ], + superposing_point_clouds=( + max_number_superposing_point_clouds + ), + point_cloud_resolution=average_distance_point_cloud, + ), + ) + # epsg_cloud and optimal_terrain_tile_width have the same epsg + self.optimal_terrain_tile_width = ( + preprocessing.convert_optimal_tile_size_with_epsg( + self.terrain_bounds, + self.optimal_terrain_tile_width, + self.epsg, + epsg_cloud, + ) + ) + + @cars_profile(name="run_dense_pipeline", interval=0.5) + def run(self): # noqa C901 + """ + Run pipeline + + """ + + self.out_dir = self.used_conf[OUTPUT][out_cst.OUT_DIRECTORY] + self.dump_dir = os.path.join(self.out_dir, "dump_dir") + self.auxiliary = self.used_conf[OUTPUT][out_cst.AUXILIARY] + + # Save used conf + cars_dataset.save_dict( + self.used_conf, + os.path.join(self.out_dir, "used_conf.json"), + safe_save=True, + ) + + # start cars orchestrator + with orchestrator.Orchestrator( + orchestrator_conf=self.used_conf[ORCHESTRATOR], + out_dir=self.out_dir, + out_json_path=os.path.join( + self.out_dir, + out_cst.INFO_FILENAME, + ), + ) as self.cars_orchestrator: + + # initialize out_json + self.cars_orchestrator.update_out_info( + { + "version": __version__, + "pipeline": "default", + "inputs": self.used_conf[INPUTS], + } + ) + + if self.compute_depth_map: + self.sensor_to_depth_maps() + else: + self.load_input_depth_maps() + + if self.save_output_dsm or self.save_output_point_cloud: + end_pipeline = self.preprocess_depth_maps() + + if self.save_output_dsm and not end_pipeline: + self.rasterize_point_cloud() + + # Cleaning: delete everything in tile_processing if + # save_intermediate_data is not activated + if not self.used_conf[ADVANCED][adv_cst.SAVE_INTERMEDIATE_DATA]: + self.cars_orchestrator.add_to_clean( + os.path.join(self.dump_dir, "tile_processing") + ) diff --git a/cars/pipelines/depth_maps_to_dsm/__init__.py b/cars/pipelines/depth_maps_to_dsm/__init__.py deleted file mode 100644 index 16bb5cd3..00000000 --- a/cars/pipelines/depth_maps_to_dsm/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python -# coding: utf8 -# -# Copyright (c) 2020 Centre National d'Etudes Spatiales (CNES). -# -# This file is part of CARS -# (see https://github.com/CNES/cars). -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -""" -CARS sensor_to_dense_dsm pipeline module init file -""" - -# Cars imports -from cars.pipelines.depth_maps_to_dsm import ( # noqa: F401 - depth_maps_to_dsm_pipeline, -) diff --git a/cars/pipelines/depth_maps_to_dsm/depth_maps_to_dsm_pipeline.py b/cars/pipelines/depth_maps_to_dsm/depth_maps_to_dsm_pipeline.py deleted file mode 100644 index f4f8113c..00000000 --- a/cars/pipelines/depth_maps_to_dsm/depth_maps_to_dsm_pipeline.py +++ /dev/null @@ -1,629 +0,0 @@ -#!/usr/bin/env python -# coding: utf8 -# -# Copyright (c) 2020 Centre National d'Etudes Spatiales (CNES). -# -# This file is part of CARS -# (see https://github.com/CNES/cars). -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -""" -CARS point cloud to full resolution dsm pipeline class file -""" - -# Standard imports -from __future__ import print_function - -import json -import logging -import os - -# CARS imports -from cars import __version__ -from cars.applications.application import Application -from cars.applications.point_cloud_fusion import pc_tif_tools -from cars.core import preprocessing, roi_tools -from cars.data_structures import cars_dataset -from cars.orchestrator import orchestrator -from cars.orchestrator.cluster.log_wrapper import cars_profile -from cars.pipelines.parameters import advanced_parameters -from cars.pipelines.parameters import advanced_parameters_constants as adv_cst -from cars.pipelines.parameters import depth_map_inputs -from cars.pipelines.parameters import ( - depth_map_inputs_constants as depth_map_cst, -) -from cars.pipelines.parameters import output_constants, output_parameters -from cars.pipelines.parameters import sensor_inputs_constants as sens_cst -from cars.pipelines.pipeline import Pipeline -from cars.pipelines.pipeline_constants import ( - ADVANCED, - APPLICATIONS, - INPUTS, - ORCHESTRATOR, - OUTPUT, - PIPELINE, -) -from cars.pipelines.pipeline_template import PipelineTemplate - - -@Pipeline.register( - "dense_depth_maps_to_dense_dsm_no_merging", - "dense_depth_maps_to_dense_dsm", -) -class DepthMapsToDsmPipeline(PipelineTemplate): - """ - DepthMapsToDsmPipeline - """ - - # pylint: disable=too-many-instance-attributes - - def __init__(self, conf, config_json_dir=None): - """ - Creates pipeline - - :param pipeline_name: name of the pipeline. - :type pipeline_name: str - :param cfg: configuration {'matching_cost_method': value} - :type cfg: dictionary - :param config_json_dir: path to dir containing json - :type config_json_dir: str - """ - - # Merge parameters from associated json - # priority : cars_pipeline.json << user_inputs.json - # Get root package directory - package_path = os.path.dirname(__file__) - json_file = os.path.join( - package_path, - "..", - "conf_pipeline", - "depth_maps_to_dsm.json", - ) - - with open(json_file, "r", encoding="utf8") as fstream: - pipeline_config = json.load(fstream) - - self.conf = self.merge_pipeline_conf(pipeline_config, conf) - - # check global conf - self.check_global_schema(self.conf) - - # Used conf - self.used_conf = {} - - # Pipeline - self.used_conf[PIPELINE] = conf.get( - PIPELINE, "dense_depth_maps_to_dense_dsm" - ) - - # Check conf orchestrator - self.orchestrator_conf = self.check_orchestrator( - self.conf.get(ORCHESTRATOR, None) - ) - self.used_conf[ORCHESTRATOR] = self.orchestrator_conf - - # Check conf inputs - self.inputs = self.check_inputs( - self.conf[INPUTS], config_json_dir=config_json_dir - ) - self.used_conf[INPUTS] = self.inputs - - # Check advanced parameters - # TODO static method in the base class - self.advanced = advanced_parameters.check_advanced_parameters( - self.conf.get(ADVANCED, {}), check_epipolar_a_priori=True - ) - self.used_conf[ADVANCED] = self.advanced - - # Get ROI - ( - self.input_roi_poly, - self.input_roi_epsg, - ) = roi_tools.generate_roi_poly_from_inputs( - self.used_conf[INPUTS][sens_cst.ROI] - ) - - # Check conf output - self.output = self.check_output( - self.conf[OUTPUT], self.used_conf[PIPELINE] - ) - self.used_conf[OUTPUT] = self.output - - self.save_output_dsm = ( - "dsm" in self.output[output_constants.PRODUCT_LEVEL] - ) - - self.save_output_point_cloud = ( - "point_cloud" in self.output[output_constants.PRODUCT_LEVEL] - ) - - # Check conf application - application_conf = self.check_applications( - self.conf.get(APPLICATIONS, {}), - no_merging="no_merging" in self.used_conf[PIPELINE], - save_all_intermediate_data=self.used_conf[ADVANCED][ - adv_cst.SAVE_INTERMEDIATE_DATA - ], - save_all_point_clouds_by_pair=self.used_conf[OUTPUT].get( - output_constants.SAVE_BY_PAIR, False - ), - ) - self.used_conf[APPLICATIONS] = application_conf - - def check_inputs(self, conf, config_json_dir=None): - """ - Check the inputs given - - :param conf: configuration of inputs - :type conf: dict - :param config_json_dir: directory of used json, if - user filled paths with relative paths - :type config_json_dir: str - - :return: overloader inputs - :rtype: dict - """ - - overloaded_conf = depth_map_inputs.check_depth_maps_inputs( - conf, config_json_dir=config_json_dir - ) - - return overloaded_conf - - def check_output(self, conf, pipeline): - """ - Check the output given - - :param conf: configuration of output - :type conf: dict - :param pipeline: name of corresponding pipeline - :type pipeline_name: str - - :return overloader output - :rtype : dict - """ - return output_parameters.check_output_parameters(conf, pipeline) - - def check_applications( - self, - conf, - no_merging=False, - save_all_intermediate_data=False, - save_all_point_clouds_by_pair=False, - ): - """ - Check the given configuration for applications - - :param conf: configuration of applications - :type conf: dict - :param no_merging: True if skip PC fusion and PC removing - :type no_merging: bool - :param save_all_intermediate_data: True to save intermediate data in all - applications - :type save_all_intermediate_data: bool - :param save_all_point_clouds_by_pair: save point clouds by pair in all - relevant applications - :type save_all_point_clouds_by_pair: bool - """ - - # Check if all specified applications are used - needed_applications = [ - "point_cloud_rasterization", - ] - - if not no_merging: - needed_applications.append("point_cloud_fusion") - needed_applications.append("point_cloud_outliers_removing.1") - needed_applications.append("point_cloud_outliers_removing.2") - - for app_key in conf.keys(): - if app_key not in needed_applications: - logging.error( - "No {} application used in pipeline".format(app_key) - ) - raise NameError( - "No {} application used in pipeline".format(app_key) - ) - - # Initialize used config - used_conf = {} - for app_key in needed_applications: - used_conf[app_key] = conf.get(app_key, {}) - used_conf[app_key]["save_intermediate_data"] = used_conf[ - app_key - ].get("save_intermediate_data", save_all_intermediate_data) - - for app_key in [ - "point_cloud_fusion", - "point_cloud_outliers_removing.1", - "point_cloud_outliers_removing.2", - ]: - if app_key in needed_applications: - used_conf[app_key]["save_by_pair"] = used_conf[app_key].get( - "save_by_pair", save_all_point_clouds_by_pair - ) - - # Points cloud fusion - self.pc_fusion_application = Application( - "point_cloud_fusion", cfg=used_conf.get("point_cloud_fusion", {}) - ) - used_conf["point_cloud_fusion"] = self.pc_fusion_application.get_conf() - - # Points cloud outlier removing small components - self.pc_outliers_removing_1_app = Application( - "point_cloud_outliers_removing", - cfg=used_conf.get( - "point_cloud_outliers_removing.1", - {"method": "small_components"}, - ), - ) - used_conf["point_cloud_outliers_removing.1"] = ( - self.pc_outliers_removing_1_app.get_conf() - ) - - # Points cloud outlier removing statistical - self.pc_outliers_removing_2_app = Application( - "point_cloud_outliers_removing", - cfg=used_conf.get( - "point_cloud_outliers_removing.2", - {"method": "statistical"}, - ), - ) - used_conf["point_cloud_outliers_removing.2"] = ( - self.pc_outliers_removing_2_app.get_conf() - ) - - # Rasterization - self.rasterization_application = Application( - "point_cloud_rasterization", - cfg=used_conf.get("point_cloud_rasterization", {}), - ) - used_conf["point_cloud_rasterization"] = ( - self.rasterization_application.get_conf() - ) - - return used_conf - - @cars_profile(name="run_pc_pipeline", interval=0.5) - def run(self): - """ - Run pipeline - - """ - - out_dir = self.output[output_constants.OUT_DIRECTORY] - - # Save used conf - cars_dataset.save_dict( - self.used_conf, - os.path.join(out_dir, "used_conf.json"), - safe_save=True, - ) - - # start cars orchestrator - with orchestrator.Orchestrator( - orchestrator_conf=self.orchestrator_conf, - out_dir=out_dir, - out_json_path=os.path.join( - out_dir, - output_constants.INFO_FILENAME, - ), - ) as cars_orchestrator: - # initialize out_json - cars_orchestrator.update_out_info( - { - "version": __version__, - "pipeline": "depth_maps_to_dsm", - "inputs": self.inputs, - } - ) - - # Run applications - - # get epsg - epsg = self.output[output_constants.EPSG] - # compute epsg - epsg_cloud = pc_tif_tools.compute_epsg_from_point_cloud( - self.inputs[depth_map_cst.DEPTH_MAPS] - ) - if epsg is None: - epsg = epsg_cloud - - resolution = self.output[output_constants.RESOLUTION] - - # Compute roi polygon, in input EPSG - roi_poly = preprocessing.compute_roi_poly( - self.input_roi_poly, self.input_roi_epsg, epsg - ) - - if "no_merging" in self.used_conf[PIPELINE]: - # compute bounds - terrain_bounds = pc_tif_tools.get_bounds( - self.inputs[depth_map_cst.DEPTH_MAPS], - epsg, - roi_poly=roi_poly, - ) - - list_epipolar_points_cloud = pc_tif_tools.generate_point_clouds( - self.inputs[depth_map_cst.DEPTH_MAPS], - cars_orchestrator, - tile_size=1000, - ) - # Generate cars datasets - point_cloud_to_rasterize = ( - list_epipolar_points_cloud, - terrain_bounds, - ) - - color_type = point_cloud_to_rasterize[0][0].attributes.get( - "color_type", None - ) - - else: - # Compute terrain bounds and transform point clouds - ( - terrain_bounds, - list_epipolar_points_cloud_by_tiles, - ) = pc_tif_tools.transform_input_pc( - self.inputs[depth_map_cst.DEPTH_MAPS], - epsg, - roi_poly=roi_poly, - epipolar_tile_size=1000, # TODO change it - orchestrator=cars_orchestrator, - ) - - # Compute number of superposing point cloud for density - max_number_superposing_point_clouds = ( - pc_tif_tools.compute_max_nb_point_clouds( - list_epipolar_points_cloud_by_tiles - ) - ) - - # Compute average distance between two points - average_distance_point_cloud = ( - pc_tif_tools.compute_average_distance( - list_epipolar_points_cloud_by_tiles - ) - ) - optimal_terrain_tile_width = min( - self.pc_outliers_removing_1_app.get_optimal_tile_size( - cars_orchestrator.cluster.checked_conf_cluster[ - "max_ram_per_worker" - ], - superposing_point_clouds=( - max_number_superposing_point_clouds - ), - point_cloud_resolution=average_distance_point_cloud, - ), - self.pc_outliers_removing_2_app.get_optimal_tile_size( - cars_orchestrator.cluster.checked_conf_cluster[ - "max_ram_per_worker" - ], - superposing_point_clouds=( - max_number_superposing_point_clouds - ), - point_cloud_resolution=average_distance_point_cloud, - ), - self.rasterization_application.get_optimal_tile_size( - cars_orchestrator.cluster.checked_conf_cluster[ - "max_ram_per_worker" - ], - superposing_point_clouds=( - max_number_superposing_point_clouds - ), - point_cloud_resolution=average_distance_point_cloud, - ), - ) - # epsg_cloud and optimal_terrain_tile_width have the same epsg - optimal_terrain_tile_width = ( - preprocessing.convert_optimal_tile_size_with_epsg( - terrain_bounds, - optimal_terrain_tile_width, - epsg, - epsg_cloud, - ) - ) - - # find which application produce the final version of the - # point cloud. The last generated point cloud will be saved - # as official point cloud product if save_output_point_cloud - # is True. - - last_pc_application = None - if ( - self.pc_outliers_removing_2_app.used_config.get( - "activated", False - ) - is True - ): - last_pc_application = "pc_outliers_removing_2" - elif ( - self.pc_outliers_removing_1_app.used_config.get( - "activated", False - ) - is True - ): - last_pc_application = "pc_outliers_removing_1" - else: - last_pc_application = "fusion" - - # Merge point clouds - merged_points_clouds = self.pc_fusion_application.run( - list_epipolar_points_cloud_by_tiles, - terrain_bounds, - epsg, - orchestrator=cars_orchestrator, - margins=( - self.pc_outliers_removing_1_app.get_on_ground_margin( - resolution=resolution - ) - + self.pc_outliers_removing_2_app.get_on_ground_margin( - resolution=resolution - ) - + self.rasterization_application.get_margins(resolution) - ), - optimal_terrain_tile_width=optimal_terrain_tile_width, - save_laz_output=self.save_output_point_cloud - and last_pc_application == "fusion", - ) - - # Add file names to retrieve source file of each point - pc_file_names = list(self.inputs[depth_map_cst.DEPTH_MAPS]) - merged_points_clouds.attributes["source_pc_names"] = ( - pc_file_names - ) - - # Remove outliers with small components method - filtered_1_merged_points_clouds = ( - self.pc_outliers_removing_1_app.run( - merged_points_clouds, - orchestrator=cars_orchestrator, - save_laz_output=self.save_output_point_cloud - and last_pc_application == "pc_outliers_removing_1", - ) - ) - - # Remove outliers with statistical components method - filtered_2_merged_points_clouds = ( - self.pc_outliers_removing_2_app.run( - filtered_1_merged_points_clouds, - orchestrator=cars_orchestrator, - save_laz_output=self.save_output_point_cloud - and last_pc_application == "pc_outliers_removing_2", - ) - ) - - point_cloud_to_rasterize = filtered_2_merged_points_clouds - - color_type = point_cloud_to_rasterize.attributes.get( - "color_type", None - ) - - rasterization_dump_dir = os.path.join( - cars_orchestrator.out_dir, "dump_dir", "rasterization" - ) - - dsm_file_name = ( - os.path.join( - out_dir, - output_constants.DSM_DIRECTORY, - "dsm.tif", - ) - if self.save_output_dsm - else None - ) - - color_file_name = ( - os.path.join( - out_dir, - output_constants.DSM_DIRECTORY, - "color.tif", - ) - if self.save_output_dsm - and self.output[output_constants.AUXILIARY][ - output_constants.AUX_COLOR - ] - else None - ) - - performance_map_file_name = ( - os.path.join( - out_dir, - output_constants.DSM_DIRECTORY, - "performance_map.tif", - ) - if self.save_output_dsm - and self.output[output_constants.AUXILIARY][ - output_constants.AUX_PERFORMANCE_MAP - ] - else None - ) - - classif_file_name = ( - os.path.join( - out_dir, - output_constants.DSM_DIRECTORY, - "classification.tif", - ) - if self.save_output_dsm - and self.output[output_constants.AUXILIARY][ - output_constants.AUX_CLASSIFICATION - ] - else None - ) - - mask_file_name = ( - os.path.join( - out_dir, - output_constants.DSM_DIRECTORY, - "mask.tif", - ) - if self.save_output_dsm - and self.output[output_constants.AUXILIARY][ - output_constants.AUX_MASK - ] - else None - ) - - contributing_pair_file_name = ( - os.path.join( - out_dir, - output_constants.DSM_DIRECTORY, - "contributing_pair.tif", - ) - if self.save_output_dsm - and self.output[output_constants.AUXILIARY][ - output_constants.AUX_CONTRIBUTING_PAIR - ] - else None - ) - - filling_file_name = ( - os.path.join( - out_dir, - output_constants.DSM_DIRECTORY, - "filling.tif", - ) - if self.save_output_dsm - and self.output[output_constants.AUXILIARY][ - output_constants.AUX_FILLING - ] - else None - ) - - # rasterize point cloud - _ = self.rasterization_application.run( - point_cloud_to_rasterize, - epsg, - resolution=resolution, - orchestrator=cars_orchestrator, - dsm_file_name=dsm_file_name, - color_file_name=color_file_name, - classif_file_name=classif_file_name, - performance_map_file_name=performance_map_file_name, - mask_file_name=mask_file_name, - contributing_pair_file_name=contributing_pair_file_name, - filling_file_name=filling_file_name, - color_dtype=color_type, - dump_dir=rasterization_dump_dir, - ) - - # Cleaning: delete everything in tile_processing if - # save_intermediate_data is not activated - if not self.advanced[adv_cst.SAVE_INTERMEDIATE_DATA]: - cars_orchestrator.add_to_clean( - os.path.join( - cars_orchestrator.out_dir, "dump_dir", "tile_processing" - ) - ) diff --git a/cars/pipelines/parameters/advanced_parameters.py b/cars/pipelines/parameters/advanced_parameters.py index ec1b2a5a..f7fb326f 100644 --- a/cars/pipelines/parameters/advanced_parameters.py +++ b/cars/pipelines/parameters/advanced_parameters.py @@ -53,6 +53,8 @@ def check_advanced_parameters(conf, check_epipolar_a_priori=True): adv_cst.DEBUG_WITH_ROI, False ) + overloaded_conf[adv_cst.MERGING] = conf.get(adv_cst.MERGING, False) + if check_epipolar_a_priori: # Check conf use_epipolar_a_priori overloaded_conf[adv_cst.USE_EPIPOLAR_A_PRIORI] = conf.get( @@ -70,6 +72,7 @@ def check_advanced_parameters(conf, check_epipolar_a_priori=True): # Validate inputs schema = { adv_cst.DEBUG_WITH_ROI: bool, + adv_cst.MERGING: bool, adv_cst.SAVE_INTERMEDIATE_DATA: bool, } if check_epipolar_a_priori: diff --git a/cars/pipelines/parameters/advanced_parameters_constants.py b/cars/pipelines/parameters/advanced_parameters_constants.py index 98d5872d..2c8015f2 100644 --- a/cars/pipelines/parameters/advanced_parameters_constants.py +++ b/cars/pipelines/parameters/advanced_parameters_constants.py @@ -30,6 +30,7 @@ USE_EPIPOLAR_A_PRIORI = "use_epipolar_a_priori" EPIPOLAR_A_PRIORI = "epipolar_a_priori" +MERGING = "merging" # inner epipolar a priori constants GRID_CORRECTION = "grid_correction" diff --git a/cars/pipelines/parameters/depth_map_inputs.py b/cars/pipelines/parameters/depth_map_inputs.py index 8303fe45..8f64e86e 100644 --- a/cars/pipelines/parameters/depth_map_inputs.py +++ b/cars/pipelines/parameters/depth_map_inputs.py @@ -31,7 +31,9 @@ # CARS imports from cars.core import constants as cst from cars.core import inputs +from cars.core.geometry.abstract_geometry import AbstractGeometry from cars.core.utils import make_relative_path_absolute +from cars.pipelines.parameters import sensor_inputs as sens_inp from cars.pipelines.parameters import sensor_inputs_constants as sens_cst @@ -55,10 +57,17 @@ def check_depth_maps_inputs(conf, config_json_dir=None): overloaded_conf[sens_cst.ROI] = conf.get(sens_cst.ROI, None) overloaded_conf[depth_map_cst.DEPTH_MAPS] = {} + overloaded_conf[sens_cst.INITIAL_ELEVATION] = ( + sens_inp.get_initial_elevation( + conf.get(sens_cst.INITIAL_ELEVATION, None) + ) + ) + # Validate inputs inputs_schema = { depth_map_cst.DEPTH_MAPS: dict, sens_cst.ROI: Or(str, dict, None), + sens_cst.INITIAL_ELEVATION: Or(dict, None), } checker_inputs = Checker(inputs_schema) @@ -193,9 +202,45 @@ def check_depth_maps_inputs(conf, config_json_dir=None): ], ) + # Check srtm dir + sens_inp.check_srtm( + overloaded_conf[sens_cst.INITIAL_ELEVATION][sens_cst.DEM_PATH] + ) + return overloaded_conf +def check_geometry_plugin(conf_inputs, conf_geom_plugin): + """ + Check the geometry plugin with inputs + :param conf_geom_plugin: name of geometry plugin + :type conf_geom_plugin: str + :param conf_inputs: checked configuration of inputs + :type conf_inputs: type + + :return: geometry plugin with dem + """ + if conf_geom_plugin is None: + conf_geom_plugin = "SharelocGeometry" + + dem_path = conf_inputs[sens_cst.INITIAL_ELEVATION][sens_cst.DEM_PATH] + + if dem_path is None: + return None + + # Initialize a geometry plugin with elevation information + geom_plugin_with_dem_and_geoid = ( + AbstractGeometry( # pylint: disable=abstract-class-instantiated + conf_geom_plugin, + dem=dem_path, + geoid=conf_inputs[sens_cst.INITIAL_ELEVATION][sens_cst.GEOID], + default_alt=sens_cst.CARS_DEFAULT_ALT, + ) + ) + + return geom_plugin_with_dem_and_geoid + + def check_input_size( x_path, y_path, z_path, mask, color, classif, filling, confidence ): diff --git a/cars/pipelines/parameters/output_parameters.py b/cars/pipelines/parameters/output_parameters.py index a629b98d..0f4891b4 100644 --- a/cars/pipelines/parameters/output_parameters.py +++ b/cars/pipelines/parameters/output_parameters.py @@ -33,9 +33,7 @@ from cars.pipelines.parameters import output_constants -def check_output_parameters( - conf, pipeline_name="sensors_to_dense_dsm_no_merging" -): +def check_output_parameters(conf): """ Check the output json configuration and fill in default values @@ -54,16 +52,15 @@ def check_output_parameters( overloaded_conf[output_constants.OUT_DIRECTORY] = out_dir overloaded_conf[output_constants.PRODUCT_LEVEL] = overloaded_conf.get( - output_constants.PRODUCT_LEVEL, get_default_product_level(pipeline_name) + output_constants.PRODUCT_LEVEL, "dsm" ) if isinstance(overloaded_conf[output_constants.PRODUCT_LEVEL], str): overloaded_conf[output_constants.PRODUCT_LEVEL] = [ overloaded_conf[output_constants.PRODUCT_LEVEL] ] - - check_product_levels( - overloaded_conf[output_constants.PRODUCT_LEVEL], pipeline_name - ) + for level in overloaded_conf[output_constants.PRODUCT_LEVEL]: + if level not in ["dsm", "depth_map", "point_cloud"]: + raise RuntimeError("Unknown product level {}".format(level)) overloaded_conf[output_constants.OUT_GEOID] = overloaded_conf.get( output_constants.OUT_GEOID, False @@ -154,50 +151,3 @@ def check_output_parameters( ) return overloaded_conf - - -def get_default_product_level(pipeline_name): - """ - Check the output json configuration and fill in default values - - :param pipeline_name: name of corresponding pipeline - :type pipeline_name: str - """ - - # sensor_to_sparse_dsm does not produce any official product - if pipeline_name == "sensors_to_sparse_dsm": - ret = [] - # sensors_to_dense_depth_maps produce a depth map - elif pipeline_name == "sensors_to_dense_depth_maps": - ret = ["depth_map"] - # other pipeline produce a dsm - else: - ret = ["dsm"] - return ret - - -def check_product_levels(product_levels, pipeline_name): - """ - Check if product levels are compatible with the pipeline - - :param pipeline_name: name of corresponding pipeline - :type pipeline_name: str - """ - - if pipeline_name == "sensors_to_sparse_dsm" and product_levels: - raise RuntimeError( - "Requested product levels {}".format(product_levels) - + ", but sensors_to_sparse_dsm does not support any product level" - ) - - for level in product_levels: - if ( - level in ["dsm", "point_cloud"] - and pipeline_name == "sensors_to_dense_depth_maps" - ): - raise RuntimeError( - "sensors_to_dense_depth_maps only supports" - + " 'depth map' output level" - ) - if level not in ["dsm", "depth_map", "point_cloud"]: - raise RuntimeError("Unknown product level {}".format(level)) diff --git a/cars/pipelines/parameters/sensor_inputs.py b/cars/pipelines/parameters/sensor_inputs.py index 8f8043eb..5620e6da 100644 --- a/cars/pipelines/parameters/sensor_inputs.py +++ b/cars/pipelines/parameters/sensor_inputs.py @@ -232,7 +232,7 @@ def check_geometry_plugin(conf_inputs, conf_advanced, conf_geom_plugin): ) ) - # If use a priori, overide initial elevation with dem_median + # If use a priori, override initial elevation with dem_median if adv_cst.USE_EPIPOLAR_A_PRIORI in conf_advanced: if conf_advanced[adv_cst.USE_EPIPOLAR_A_PRIORI]: if adv_cst.DEM_MEDIAN in conf_advanced[adv_cst.TERRAIN_A_PRIORI]: diff --git a/cars/pipelines/sensor_to_dense_dsm/__init__.py b/cars/pipelines/sensor_to_dense_dsm/__init__.py deleted file mode 100644 index fdee447b..00000000 --- a/cars/pipelines/sensor_to_dense_dsm/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python -# coding: utf8 -# -# Copyright (c) 2020 Centre National d'Etudes Spatiales (CNES). -# -# This file is part of CARS -# (see https://github.com/CNES/cars). -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -""" -CARS sensor_to_dense_dsm pipeline module init file -""" - -# Cars imports -from cars.pipelines.sensor_to_dense_dsm import ( # noqa: F401 - sensor_to_dense_dsm_pipeline, -) diff --git a/cars/pipelines/sensor_to_dense_dsm/sensor_to_dense_dsm_pipeline.py b/cars/pipelines/sensor_to_dense_dsm/sensor_to_dense_dsm_pipeline.py deleted file mode 100644 index 213e5f5a..00000000 --- a/cars/pipelines/sensor_to_dense_dsm/sensor_to_dense_dsm_pipeline.py +++ /dev/null @@ -1,1755 +0,0 @@ -#!/usr/bin/env python -# coding: utf8 -# -# Copyright (c) 2020 Centre National d'Etudes Spatiales (CNES). -# -# This file is part of CARS -# (see https://github.com/CNES/cars). -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# pylint: disable=too-many-lines -""" -CARS sensors_to_dense_dsm pipeline class file -""" -# Standard imports -from __future__ import print_function - -import json -import logging -import os - -import numpy as np - -# CARS imports -from cars import __version__ -from cars.applications.application import Application -from cars.applications.dem_generation import ( - dem_generation_constants as dem_gen_cst, -) -from cars.applications.dem_generation import dem_generation_tools -from cars.applications.grid_generation import grid_correction -from cars.applications.sparse_matching import ( - sparse_matching_tools as sparse_mtch_tools, -) -from cars.core import constants_disparity as cst_disp -from cars.core import preprocessing, roi_tools -from cars.core.geometry.abstract_geometry import AbstractGeometry -from cars.core.inputs import get_descriptions_bands -from cars.core.utils import safe_makedirs -from cars.data_structures import cars_dataset -from cars.orchestrator import orchestrator -from cars.orchestrator.cluster.log_wrapper import cars_profile -from cars.pipelines.parameters import advanced_parameters -from cars.pipelines.parameters import advanced_parameters_constants as adv_cst -from cars.pipelines.parameters import ( - output_constants, - output_parameters, - sensor_inputs, -) -from cars.pipelines.parameters import sensor_inputs_constants as sens_cst -from cars.pipelines.pipeline import Pipeline -from cars.pipelines.pipeline_constants import ( - ADVANCED, - APPLICATIONS, - GEOMETRY_PLUGIN, - INPUTS, - ORCHESTRATOR, - OUTPUT, - PIPELINE, -) -from cars.pipelines.pipeline_template import PipelineTemplate - - -@Pipeline.register( - "sensors_to_dense_dsm", - "sensors_to_dense_dsm_no_merging", - "sensors_to_dense_depth_maps", -) -class SensorToDenseDsmPipeline(PipelineTemplate): - """ - SensorToDenseDsmPipeline - """ - - # pylint: disable=too-many-instance-attributes - - def __init__(self, conf, config_json_dir=None): - """ - Creates pipeline - - :param pipeline_name: name of the pipeline. - :type pipeline_name: str - :param cfg: configuration {'matching_cost_method': value} - :type cfg: dictionary - :param config_json_dir: path to dir containing json - :type config_json_dir: str - """ - - # Used conf - self.used_conf = {} - - # Pipeline - self.used_conf[PIPELINE] = conf.get( - PIPELINE, "sensors_to_dense_dsm_no_merging" - ) - - self.generate_terrain_products = True - # set json pipeline file - if "sensors_to_dense_dsm" in self.used_conf[PIPELINE]: - json_conf_file_name = "sensor_to_dense_dsm.json" - else: - json_conf_file_name = "sensor_to_pc.json" - self.generate_terrain_products = False - - # Merge parameters from associated json - # priority : cars_pipeline.json << user_inputs.json - # Get root package directory - package_path = os.path.dirname(__file__) - json_file = os.path.join( - package_path, - "..", - "conf_pipeline", - json_conf_file_name, - ) - with open(json_file, "r", encoding="utf8") as fstream: - pipeline_config = json.load(fstream) - - self.conf = self.merge_pipeline_conf(pipeline_config, conf) - - # check global conf - self.check_global_schema(self.conf) - - # Check conf orchestrator - self.orchestrator_conf = self.check_orchestrator( - self.conf.get(ORCHESTRATOR, None) - ) - self.used_conf[ORCHESTRATOR] = self.orchestrator_conf - - # Check conf inputs - self.inputs = self.check_inputs( - self.conf[INPUTS], config_json_dir=config_json_dir - ) - - # Check advanced parameters - # TODO static method in the base class - self.advanced = advanced_parameters.check_advanced_parameters( - self.conf.get(ADVANCED, {}), check_epipolar_a_priori=True - ) - self.used_conf[ADVANCED] = self.advanced - - # Check geometry plugin and overwrite geomodel in conf inputs - ( - self.inputs, - self.used_conf[GEOMETRY_PLUGIN], - self.geom_plugin_without_dem_and_geoid, - self.geom_plugin_with_dem_and_geoid, - self.dem_generation_roi, - ) = sensor_inputs.check_geometry_plugin( - self.inputs, self.advanced, self.conf.get(GEOMETRY_PLUGIN, None) - ) - self.used_conf[INPUTS] = self.inputs - - # Get ROI - ( - self.input_roi_poly, - self.input_roi_epsg, - ) = roi_tools.generate_roi_poly_from_inputs( - self.used_conf[INPUTS][sens_cst.ROI] - ) - - self.debug_with_roi = self.used_conf[ADVANCED][adv_cst.DEBUG_WITH_ROI] - # Check conf output - self.output = self.check_output( - self.conf[OUTPUT], self.used_conf[PIPELINE] - ) - self.used_conf[OUTPUT] = self.output - - self.save_output_dsm = ( - "dsm" in self.output[output_constants.PRODUCT_LEVEL] - ) - self.save_output_depth_map = ( - "depth_map" in self.output[output_constants.PRODUCT_LEVEL] - ) - self.save_output_point_cloud = ( - "point_cloud" in self.output[output_constants.PRODUCT_LEVEL] - ) - - # Check conf application - self.application_conf = self.check_applications( - self.conf.get(APPLICATIONS, {}), - self.generate_terrain_products, - no_merging="no_merging" in self.used_conf[PIPELINE], - save_all_intermediate_data=self.used_conf[ADVANCED][ - adv_cst.SAVE_INTERMEDIATE_DATA - ], - save_all_point_clouds_by_pair=self.used_conf[OUTPUT].get( - output_constants.SAVE_BY_PAIR, False - ), - ) - - # Check conf application vs inputs application - self.application_conf = self.check_applications_with_inputs( - self.inputs, self.application_conf - ) - - self.used_conf[APPLICATIONS] = self.application_conf - - @staticmethod - def check_inputs(conf, config_json_dir=None): - """ - Check the inputs given - - :param conf: configuration of inputs - :type conf: dict - :param config_json_dir: directory of used json, if - user filled paths with relative paths - :type config_json_dir: str - - :return: overloader inputs - :rtype: dict - """ - return sensor_inputs.sensors_check_inputs( - conf, config_json_dir=config_json_dir - ) - - @staticmethod - def check_output(conf, pipeline): - """ - Check the output given - - :param conf: configuration of output - :type conf: dict - :param pipeline: name of corresponding pipeline - :type pipeline_name: str - - :return overloader output - :rtype : dict - """ - return output_parameters.check_output_parameters(conf, pipeline) - - def check_applications( - self, - conf, - generate_terrain_products, - no_merging=False, - save_all_intermediate_data=False, - save_all_point_clouds_by_pair=False, - ): - """ - Check the given configuration for applications, - and generates needed applications for pipeline. - - :param conf: configuration of applications - :type conf: dict - :param generate_terrain_products: true if uses point cloud - fusion, pc removing, rasterization - :type generate_terrain_products: bool - :param no_merging: True if skip PC fusion and PC removing - :type no_merging: bool - :param save_all_intermediate_data: True to save intermediate data in all - applications - :type save_all_intermediate_data: bool - :param save_all_point_clouds_by_pair: save point clouds by pair in all - relevant applications - :type save_all_point_clouds_by_pair: bool - """ - - # Check if all specified applications are used - # Application in terrain_application are note used in - # the sensors_to_dense_depth_maps pipeline - needed_applications = [ - "grid_generation", - "resampling", - "holes_detection", - "dense_matches_filling.1", - "dense_matches_filling.2", - "sparse_matching", - "dense_matching", - "triangulation", - "pc_denoising", - "dem_generation", - ] - - terrain_applications = [ - "point_cloud_rasterization", - ] - - if not no_merging: - terrain_applications.append("point_cloud_fusion") - terrain_applications.append("point_cloud_outliers_removing.1") - terrain_applications.append("point_cloud_outliers_removing.2") - - pipeline_name = "sensors_to_dense_depth_maps" - if generate_terrain_products: - needed_applications += terrain_applications - pipeline_name = "sensors_to_dense_dsm" - if no_merging: - pipeline_name += "_no_merging" - - for app_key in conf.keys(): - if app_key not in needed_applications: - logging.error( - "No {} application used in pipeline {}".format( - app_key, pipeline_name - ) - ) - raise NameError( - "No {} application used in pipeline {}".format( - app_key, pipeline_name - ) - ) - - # Initialize used config - used_conf = {} - for app_key in needed_applications: - used_conf[app_key] = conf.get(app_key, {}) - used_conf[app_key]["save_intermediate_data"] = used_conf[ - app_key - ].get("save_intermediate_data", save_all_intermediate_data) - - for app_key in [ - "point_cloud_fusion", - "point_cloud_outliers_removing.1", - "point_cloud_outliers_removing.2", - "pc_denoising", - ]: - if app_key in needed_applications: - used_conf[app_key]["save_by_pair"] = used_conf[app_key].get( - "save_by_pair", save_all_point_clouds_by_pair - ) - - # Epipolar grid generation - self.epipolar_grid_generation_application = Application( - "grid_generation", cfg=used_conf.get("grid_generation", {}) - ) - used_conf["grid_generation"] = ( - self.epipolar_grid_generation_application.get_conf() - ) - - # image resampling - self.resampling_application = Application( - "resampling", cfg=used_conf.get("resampling", {}) - ) - used_conf["resampling"] = self.resampling_application.get_conf() - - # holes detection - self.holes_detection_app = Application( - "holes_detection", cfg=used_conf.get("holes_detection", {}) - ) - used_conf["holes_detection"] = self.holes_detection_app.get_conf() - - # disparity filling 1 plane - self.dense_matches_filling_1 = Application( - "dense_matches_filling", - cfg=used_conf.get( - "dense_matches_filling.1", - {"method": "plane"}, - ), - ) - used_conf["dense_matches_filling.1"] = ( - self.dense_matches_filling_1.get_conf() - ) - - # disparity filling 2 - self.dense_matches_filling_2 = Application( - "dense_matches_filling", - cfg=used_conf.get( - "dense_matches_filling.2", - {"method": "zero_padding"}, - ), - ) - used_conf["dense_matches_filling.2"] = ( - self.dense_matches_filling_2.get_conf() - ) - - # Sparse Matching - self.sparse_mtch_app = Application( - "sparse_matching", cfg=used_conf.get("sparse_matching", {}) - ) - used_conf["sparse_matching"] = self.sparse_mtch_app.get_conf() - - # Matching - generate_performance_map = ( - self.used_conf[OUTPUT] - .get(output_constants.AUXILIARY, {}) - .get(output_constants.AUX_PERFORMANCE_MAP, False) - ) - dense_matching_config = used_conf.get("dense_matching", {}) - if generate_performance_map is True: - dense_matching_config["generate_performance_map"] = True - self.dense_matching_app = Application( - "dense_matching", cfg=dense_matching_config - ) - used_conf["dense_matching"] = self.dense_matching_app.get_conf() - - # Triangulation - self.triangulation_application = Application( - "triangulation", cfg=used_conf.get("triangulation", {}) - ) - used_conf["triangulation"] = self.triangulation_application.get_conf() - - self.pc_denoising_application = Application( - "pc_denoising", - cfg=used_conf.get("pc_denoising", {"method": "none"}), - ) - - # MNT generation - self.dem_generation_application = Application( - "dem_generation", cfg=used_conf.get("dem_generation", {}) - ) - used_conf["dem_generation"] = self.dem_generation_application.get_conf() - - if generate_terrain_products: - # Points cloud fusion - self.pc_fusion_application = Application( - "point_cloud_fusion", - cfg=used_conf.get("point_cloud_fusion", {}), - ) - if not no_merging: - used_conf["point_cloud_fusion"] = ( - self.pc_fusion_application.get_conf() - ) - - # Points cloud outlier removing small components - self.pc_outliers_removing_1_app = Application( - "point_cloud_outliers_removing", - cfg=used_conf.get( - "point_cloud_outliers_removing.1", - {"method": "small_components"}, - ), - ) - if not no_merging: - used_conf["point_cloud_outliers_removing.1"] = ( - self.pc_outliers_removing_1_app.get_conf() - ) - - # Points cloud outlier removing statistical - self.pc_outliers_removing_2_app = Application( - "point_cloud_outliers_removing", - cfg=used_conf.get( - "point_cloud_outliers_removing.2", - {"method": "statistical"}, - ), - ) - if not no_merging: - used_conf["point_cloud_outliers_removing.2"] = ( - self.pc_outliers_removing_2_app.get_conf() - ) - - # Rasterization - self.rasterization_application = Application( - "point_cloud_rasterization", - cfg=used_conf.get("point_cloud_rasterization", {}), - ) - used_conf["point_cloud_rasterization"] = ( - self.rasterization_application.get_conf() - ) - else: - # Points cloud fusion - self.pc_fusion_application = None - # Points cloud outlier removing small components - self.pc_outliers_removing_1_app = None - # Points cloud outlier removing statistical - self.pc_outliers_removing_2_app = None - # Rasterization - self.rasterization_application = None - - return used_conf - - def check_applications_with_inputs(self, inputs_conf, application_conf): - """ - Check for each application the input and output configuration - consistency - - :param inputs_conf: inputs checked configuration - :type inputs_conf: dict - :param application_conf: application checked configuration - :type application_conf: dict - """ - - initial_elevation = self.inputs["initial_elevation"]["dem"] is not None - if self.sparse_mtch_app.elevation_delta_lower_bound is None: - self.sparse_mtch_app.used_config["elevation_delta_lower_bound"] = ( - -100 if initial_elevation else -1000 - ) - self.sparse_mtch_app.elevation_delta_lower_bound = ( - self.sparse_mtch_app.used_config["elevation_delta_lower_bound"] - ) - if self.sparse_mtch_app.elevation_delta_upper_bound is None: - self.sparse_mtch_app.used_config["elevation_delta_upper_bound"] = ( - 1000 if initial_elevation else 9000 - ) - self.sparse_mtch_app.elevation_delta_upper_bound = ( - self.sparse_mtch_app.used_config["elevation_delta_upper_bound"] - ) - application_conf["sparse_matching"] = self.sparse_mtch_app.get_conf() - - # check classification application parameter compare - # to each sensors inputs classification list - for application_key in application_conf: - if "classification" in application_conf[application_key]: - for item in inputs_conf["sensors"]: - if "classification" in inputs_conf["sensors"][item].keys(): - if inputs_conf["sensors"][item]["classification"]: - descriptions = get_descriptions_bands( - inputs_conf["sensors"][item]["classification"] - ) - if application_conf[application_key][ - "classification" - ] and not set( - application_conf[application_key][ - "classification" - ] - ).issubset( - set(descriptions) - ): - raise RuntimeError( - "The {} bands description {} ".format( - inputs_conf["sensors"][item][ - "classification" - ], - list(descriptions), - ) - + "and the {} config are not ".format( - application_key - ) - + "consistent: {}".format( - application_conf[application_key][ - "classification" - ] - ) - ) - for key1, key2 in inputs_conf["pairing"]: - corr_cfg = self.dense_matching_app.loader.get_conf() - img_left = inputs_conf["sensors"][key1]["image"] - img_right = inputs_conf["sensors"][key2]["image"] - classif_left = None - classif_right = None - if "classification" in inputs_conf["sensors"][key1]: - classif_left = inputs_conf["sensors"][key1]["classification"] - if "classification" in inputs_conf["sensors"][key2]: - classif_right = inputs_conf["sensors"][key2]["classification"] - self.dense_matching_app.corr_config = ( - self.dense_matching_app.loader.check_conf( - corr_cfg, - img_left, - img_right, - classif_left, - classif_right, - ) - ) - - return application_conf - - @cars_profile(name="run_dense_pipeline", interval=0.5) - def run(self): # noqa C901 - """ - Run pipeline - - """ - - out_dir = self.output[output_constants.OUT_DIRECTORY] - auxiliary = self.output[output_constants.AUXILIARY] - - # Save used conf - cars_dataset.save_dict( - self.used_conf, - os.path.join(out_dir, "used_conf.json"), - safe_save=True, - ) - - # start cars orchestrator - with orchestrator.Orchestrator( - orchestrator_conf=self.orchestrator_conf, - out_dir=out_dir, - out_json_path=os.path.join( - out_dir, - output_constants.INFO_FILENAME, - ), - ) as cars_orchestrator: - # initialize out_json - cars_orchestrator.update_out_info( - { - "version": __version__, - "pipeline": "sensor_to_dense_dsm_pipeline", - "inputs": self.inputs, - } - ) - - # Application dump directory - dump_dir = os.path.join(cars_orchestrator.out_dir, "dump_dir") - - # Run applications - - # Initialize epsg for terrain tiles - epsg = self.output[output_constants.EPSG] - if epsg is not None: - # Compute roi polygon, in output EPSG - roi_poly = preprocessing.compute_roi_poly( - self.input_roi_poly, self.input_roi_epsg, epsg - ) - - resolution = self.output[output_constants.RESOLUTION] - - # List of terrain roi corresponding to each epipolar pair - # Used to generate final terrain roi - list_terrain_roi = [] - - # initialise lists of points - list_epipolar_points_cloud = [] - list_sensor_pairs = sensor_inputs.generate_inputs( - self.inputs, self.geom_plugin_without_dem_and_geoid - ) - logging.info( - "Received {} stereo pairs configurations".format( - len(list_sensor_pairs) - ) - ) - - # pairs is a dict used to store the CarsDataset of - # all pairs, easily retrievable with pair keys - pairs = {} - - # triangulated_matches_list is used to store triangulated matche - # used in dem generation - triangulated_matches_list = [] - - for ( - pair_key, - sensor_image_left, - sensor_image_right, - ) in list_sensor_pairs: - # initialize pairs for current pair - pairs[pair_key] = {} - pairs[pair_key]["sensor_image_left"] = sensor_image_left - pairs[pair_key]["sensor_image_right"] = sensor_image_right - - # Run applications - - # Run grid generation - # We generate grids with dem if it is provided. - # If not provided, grid are generated without dem and a dem - # will be generated, to use later for a new grid generation** - altitude_delta_min = self.inputs.get( - sens_cst.INITIAL_ELEVATION, {} - ).get(sens_cst.ALTITUDE_DELTA_MIN, None) - altitude_delta_max = self.inputs.get( - sens_cst.INITIAL_ELEVATION, {} - ).get(sens_cst.ALTITUDE_DELTA_MAX, None) - - if ( - self.inputs[sens_cst.INITIAL_ELEVATION][sens_cst.DEM_PATH] - is None - ): - geom_plugin = self.geom_plugin_without_dem_and_geoid - - if None not in (altitude_delta_min, altitude_delta_max): - raise RuntimeError( - "Dem path is mandatory for " - "the use of altitude deltas" - ) - else: - geom_plugin = self.geom_plugin_with_dem_and_geoid - - # Generate rectification grids - ( - pairs[pair_key]["grid_left"], - pairs[pair_key]["grid_right"], - ) = self.epipolar_grid_generation_application.run( - pairs[pair_key]["sensor_image_left"], - pairs[pair_key]["sensor_image_right"], - geom_plugin, - orchestrator=cars_orchestrator, - pair_folder=os.path.join( - dump_dir, - "epipolar_grid_generation", - "initial", - pair_key, - ), - pair_key=pair_key, - ) - - # Run holes detection - # Get classif depending on which filling is used - # For now, 2 filling application can be used, and be configured - # with any order. the .1 will be performed before the .2 - pairs[pair_key]["holes_classif"] = [] - pairs[pair_key]["holes_poly_margin"] = 0 - add_classif = False - if self.dense_matches_filling_1.used_method == "plane": - pairs[pair_key][ - "holes_classif" - ] += self.dense_matches_filling_1.get_classif() - pairs[pair_key]["holes_poly_margin"] = max( - pairs[pair_key]["holes_poly_margin"], - self.dense_matches_filling_1.get_poly_margin(), - ) - add_classif = True - if self.dense_matches_filling_2.used_method == "plane": - pairs[pair_key][ - "holes_classif" - ] += self.dense_matches_filling_2.get_classif() - pairs[pair_key]["holes_poly_margin"] = max( - pairs[pair_key]["holes_poly_margin"], - self.dense_matches_filling_2.get_poly_margin(), - ) - add_classif = True - - pairs[pair_key]["holes_bbox_left"] = [] - pairs[pair_key]["holes_bbox_right"] = [] - - if self.used_conf[ADVANCED][ - adv_cst.USE_EPIPOLAR_A_PRIORI - ] is False or (len(pairs[pair_key]["holes_classif"]) > 0): - # Run resampling only if needed: - # no a priori or needs to detect holes - - # Run epipolar resampling - ( - pairs[pair_key]["epipolar_image_left"], - pairs[pair_key]["epipolar_image_right"], - ) = self.resampling_application.run( - pairs[pair_key]["sensor_image_left"], - pairs[pair_key]["sensor_image_right"], - pairs[pair_key]["grid_left"], - pairs[pair_key]["grid_right"], - orchestrator=cars_orchestrator, - pair_folder=os.path.join( - dump_dir, "resampling", "initial", pair_key - ), - pair_key=pair_key, - margins_fun=self.sparse_mtch_app.get_margins_fun(), - tile_width=None, - tile_height=None, - add_color=False, - add_classif=add_classif, - ) - - # Generate the holes polygons in epipolar images - # They are only generated if dense_matches_filling - # applications are used later - ( - pairs[pair_key]["holes_bbox_left"], - pairs[pair_key]["holes_bbox_right"], - ) = self.holes_detection_app.run( - pairs[pair_key]["epipolar_image_left"], - pairs[pair_key]["epipolar_image_right"], - classification=pairs[pair_key]["holes_classif"], - margin=pairs[pair_key]["holes_poly_margin"], - orchestrator=cars_orchestrator, - pair_folder=os.path.join( - dump_dir, "hole_detection", pair_key - ), - pair_key=pair_key, - ) - - if ( - self.used_conf[ADVANCED][adv_cst.USE_EPIPOLAR_A_PRIORI] - is False - ): - # Run epipolar sparse_matching application - ( - pairs[pair_key]["epipolar_matches_left"], - _, - ) = self.sparse_mtch_app.run( - pairs[pair_key]["epipolar_image_left"], - pairs[pair_key]["epipolar_image_right"], - pairs[pair_key]["grid_left"].attributes[ - "disp_to_alt_ratio" - ], - orchestrator=cars_orchestrator, - pair_folder=os.path.join( - dump_dir, "sparse_matching", pair_key - ), - pair_key=pair_key, - ) - - # Run cluster breakpoint to compute sifts: force computation - cars_orchestrator.breakpoint() - - # Run grid correction application - save_corrected_grid = ( - self.epipolar_grid_generation_application.get_save_grids() - ) - if ( - self.used_conf[ADVANCED][adv_cst.USE_EPIPOLAR_A_PRIORI] - is False - ): - # Estimate grid correction if no epipolar a priori - # Filter and save matches - pairs[pair_key]["matches_array"] = ( - self.sparse_mtch_app.filter_matches( - pairs[pair_key]["epipolar_matches_left"], - pairs[pair_key]["grid_left"], - pairs[pair_key]["grid_right"], - orchestrator=cars_orchestrator, - pair_key=pair_key, - pair_folder=os.path.join( - dump_dir, "sparse_matching", pair_key - ), - save_matches=( - self.sparse_mtch_app.get_save_matches() - ), - ) - ) - # Compute grid correction - ( - pairs[pair_key]["grid_correction_coef"], - pairs[pair_key]["corrected_matches_array"], - _, - _, - _, - ) = grid_correction.estimate_right_grid_correction( - pairs[pair_key]["matches_array"], - pairs[pair_key]["grid_right"], - save_matches=self.sparse_mtch_app.get_save_matches(), - pair_folder=os.path.join( - dump_dir, "grid_correction", "initial", pair_key - ), - pair_key=pair_key, - orchestrator=cars_orchestrator, - ) - # Correct grid right - pairs[pair_key]["corrected_grid_right"] = ( - grid_correction.correct_grid( - pairs[pair_key]["grid_right"], - pairs[pair_key]["grid_correction_coef"], - save_corrected_grid, - os.path.join( - dump_dir, "grid_correction", "initial", pair_key - ), - ) - ) - - # Clean grid at the end of processing if required - if not save_corrected_grid: - cars_orchestrator.add_to_clean( - os.path.join( - dump_dir, "grid_correction", "initial", pair_key - ) - ) - - pairs[pair_key]["corrected_grid_left"] = pairs[pair_key][ - "grid_left" - ] - - # Triangulate matches - pairs[pair_key]["triangulated_matches"] = ( - dem_generation_tools.triangulate_sparse_matches( - pairs[pair_key]["sensor_image_left"], - pairs[pair_key]["sensor_image_right"], - pairs[pair_key]["grid_left"], - pairs[pair_key]["corrected_grid_right"], - pairs[pair_key]["corrected_matches_array"], - geom_plugin, - ) - ) - - # filter triangulated_matches - matches_filter_knn = ( - self.sparse_mtch_app.get_matches_filter_knn() - ) - matches_filter_dev_factor = ( - self.sparse_mtch_app.get_matches_filter_dev_factor() - ) - pairs[pair_key]["filtered_triangulated_matches"] = ( - sparse_mtch_tools.filter_point_cloud_matches( - pairs[pair_key]["triangulated_matches"], - matches_filter_knn=matches_filter_knn, - matches_filter_dev_factor=matches_filter_dev_factor, - ) - ) - - triangulated_matches_list.append( - pairs[pair_key]["filtered_triangulated_matches"] - ) - - if self.used_conf[ADVANCED][adv_cst.USE_EPIPOLAR_A_PRIORI]: - # Use a priori - dem_median = self.used_conf[ADVANCED][adv_cst.TERRAIN_A_PRIORI][ - adv_cst.DEM_MEDIAN - ] - dem_min = self.used_conf[ADVANCED][adv_cst.TERRAIN_A_PRIORI][ - adv_cst.DEM_MIN - ] - dem_max = self.used_conf[ADVANCED][adv_cst.TERRAIN_A_PRIORI][ - adv_cst.DEM_MAX - ] - altitude_delta_min = self.used_conf[ADVANCED][ - adv_cst.TERRAIN_A_PRIORI - ][adv_cst.ALTITUDE_DELTA_MIN] - altitude_delta_max = self.used_conf[ADVANCED][ - adv_cst.TERRAIN_A_PRIORI - ][adv_cst.ALTITUDE_DELTA_MAX] - else: - dem_generation_output_dir = os.path.join( - dump_dir, "dem_generation" - ) - safe_makedirs(dem_generation_output_dir) - # Use initial elevation if provided, and generate dems - # Generate MNT from matches - dem = self.dem_generation_application.run( - triangulated_matches_list, - dem_generation_output_dir, - self.inputs[sens_cst.INITIAL_ELEVATION][sens_cst.GEOID], - dem_roi_to_use=self.dem_generation_roi, - ) - # Same geometry plugin if we use exogenous dem - # as initial elevation always used before if provided - dem_median = dem.attributes[dem_gen_cst.DEM_MEDIAN_PATH] - if ( - self.inputs[sens_cst.INITIAL_ELEVATION][sens_cst.DEM_PATH] - is not None - ): - dem_median = self.inputs[sens_cst.INITIAL_ELEVATION][ - sens_cst.DEM_PATH - ] - - if ( - dem_median - != self.inputs[sens_cst.INITIAL_ELEVATION][ - sens_cst.DEM_PATH - ] - ): - self.geom_plugin_with_dem_and_geoid = ( - sensor_inputs.generate_geometry_plugin_with_dem( - self.used_conf[GEOMETRY_PLUGIN], - self.inputs, - dem=dem_median, - crop_dem=False, - ) - ) - - dem_min = dem.attributes[dem_gen_cst.DEM_MIN_PATH] - dem_max = dem.attributes[dem_gen_cst.DEM_MAX_PATH] - - # update used configuration with terrain a priori - if None not in (altitude_delta_min, altitude_delta_max): - advanced_parameters.update_conf( - self.used_conf, - dem_median=dem_median, - altitude_delta_min=altitude_delta_min, - altitude_delta_max=altitude_delta_max, - ) - else: - advanced_parameters.update_conf( - self.used_conf, - dem_median=dem_median, - dem_min=dem_min, - dem_max=dem_max, - ) - - # Define param - use_global_disp_range = ( - self.dense_matching_app.use_global_disp_range - ) - - if self.pc_denoising_application is not None: - denoising_overload_fun = ( - self.pc_denoising_application.get_triangulation_overload() - ) - else: - denoising_overload_fun = None - - pairs_names = [pair_name for pair_name, _, _ in list_sensor_pairs] - - for cloud_id, (pair_key, _, _) in enumerate(list_sensor_pairs): - # Geometry plugin with dem will be used for the grid generation - geom_plugin = self.geom_plugin_with_dem_and_geoid - if ( - self.used_conf[ADVANCED][adv_cst.USE_EPIPOLAR_A_PRIORI] - is False - ): - - if ( - self.inputs[sens_cst.INITIAL_ELEVATION][ - sens_cst.DEM_PATH - ] - is None - ): - # Generate grids with new MNT - ( - pairs[pair_key]["new_grid_left"], - pairs[pair_key]["new_grid_right"], - ) = self.epipolar_grid_generation_application.run( - pairs[pair_key]["sensor_image_left"], - pairs[pair_key]["sensor_image_right"], - geom_plugin, - orchestrator=cars_orchestrator, - pair_folder=os.path.join( - dump_dir, - "epipolar_grid_generation", - "new_mnt", - pair_key, - ), - pair_key=pair_key, - ) - - # Correct grids with former matches - # Transform matches to new grids - - new_grid_matches_array = ( - AbstractGeometry.transform_matches_from_grids( - pairs[pair_key]["corrected_matches_array"], - pairs[pair_key]["corrected_grid_left"], - pairs[pair_key]["corrected_grid_right"], - pairs[pair_key]["new_grid_left"], - pairs[pair_key]["new_grid_right"], - ) - ) - # Estimate grid_correction - ( - pairs[pair_key]["grid_correction_coef"], - pairs[pair_key]["corrected_matches_array"], - _, - _, - _, - ) = grid_correction.estimate_right_grid_correction( - new_grid_matches_array, - pairs[pair_key]["new_grid_right"], - save_matches=( - self.sparse_mtch_app.get_save_matches() - ), - pair_folder=os.path.join( - dump_dir, "grid_correction", "new", pair_key - ), - pair_key=pair_key, - orchestrator=cars_orchestrator, - ) - - # Correct grid right - - pairs[pair_key]["corrected_grid_right"] = ( - grid_correction.correct_grid( - pairs[pair_key]["new_grid_right"], - pairs[pair_key]["grid_correction_coef"], - save_corrected_grid, - os.path.join( - dump_dir, "grid_correction", "new", pair_key - ), - ) - ) - - if not save_corrected_grid: - cars_orchestrator.add_to_clean( - os.path.join( - dump_dir, "grid_correction", "new", pair_key - ) - ) - - # Use the new grid as uncorrected grid - pairs[pair_key]["grid_right"] = pairs[pair_key][ - "new_grid_right" - ] - - pairs[pair_key]["corrected_grid_left"] = pairs[ - pair_key - ]["new_grid_left"] - - # matches filter params - matches_filter_knn = ( - self.sparse_mtch_app.get_matches_filter_knn() - ) - matches_filter_dev_factor = ( - self.sparse_mtch_app.get_matches_filter_dev_factor() - ) - if use_global_disp_range: - # Triangulate new matches - pairs[pair_key]["triangulated_matches"] = ( - dem_generation_tools.triangulate_sparse_matches( - pairs[pair_key]["sensor_image_left"], - pairs[pair_key]["sensor_image_right"], - pairs[pair_key]["corrected_grid_left"], - pairs[pair_key]["corrected_grid_right"], - pairs[pair_key]["corrected_matches_array"], - geometry_plugin=geom_plugin, - ) - ) - # filter triangulated_matches - # Filter outliers - pairs[pair_key]["filtered_triangulated_matches"] = ( - sparse_mtch_tools.filter_point_cloud_matches( - pairs[pair_key]["triangulated_matches"], - matches_filter_knn=matches_filter_knn, - matches_filter_dev_factor=( - matches_filter_dev_factor - ), - ) - ) - - if use_global_disp_range: - # Compute disp_min and disp_max - ( - dmin, - dmax, - ) = sparse_mtch_tools.compute_disp_min_disp_max( - pairs[pair_key]["filtered_triangulated_matches"], - cars_orchestrator, - disp_margin=( - self.sparse_mtch_app.get_disparity_margin() - ), - pair_key=pair_key, - disp_to_alt_ratio=pairs[pair_key][ - "corrected_grid_left" - ].attributes["disp_to_alt_ratio"], - ) - else: - # Use epipolar a priori - # load the disparity range - [dmin, dmax] = self.used_conf[ADVANCED][ - adv_cst.EPIPOLAR_A_PRIORI - ][pair_key][adv_cst.DISPARITY_RANGE] - # load the grid correction coefficient - pairs[pair_key]["grid_correction_coef"] = self.used_conf[ - ADVANCED - ][adv_cst.EPIPOLAR_A_PRIORI][pair_key][ - adv_cst.GRID_CORRECTION - ] - pairs[pair_key]["corrected_grid_left"] = pairs[pair_key][ - "grid_left" - ] - # no correction if the grid correction coefs are None - if pairs[pair_key]["grid_correction_coef"] is None: - pairs[pair_key]["corrected_grid_right"] = pairs[ - pair_key - ]["grid_right"] - else: - # Correct grid right with provided epipolar a priori - pairs[pair_key]["corrected_grid_right"] = ( - grid_correction.correct_grid_from_1d( - pairs[pair_key]["grid_right"], - pairs[pair_key]["grid_correction_coef"], - save_corrected_grid, - os.path.join( - dump_dir, "grid_correction", pair_key - ), - ) - ) - - # Run epipolar resampling - - # Update used_conf configuration with epipolar a priori - # Add global min and max computed with grids - advanced_parameters.update_conf( - self.used_conf, - grid_correction_coef=pairs[pair_key][ - "grid_correction_coef" - ], - pair_key=pair_key, - ) - # saved used configuration - cars_dataset.save_dict( - self.used_conf, - os.path.join(out_dir, "used_conf.json"), - safe_save=True, - ) - - # Generate min and max disp grids - # Global disparity min and max will be computed from - # these grids - dense_matching_pair_folder = os.path.join( - dump_dir, "dense_matching", pair_key - ) - if use_global_disp_range: - # Generate min and max disp grids from constants - # sensor image is not used here - # TODO remove when only local diparity range will be used - disp_range_grid = ( - self.dense_matching_app.generate_disparity_grids( - pairs[pair_key]["sensor_image_right"], - pairs[pair_key]["corrected_grid_right"], - self.geom_plugin_with_dem_and_geoid, - dmin=dmin, - dmax=dmax, - pair_folder=dense_matching_pair_folder, - ) - ) - elif None in (altitude_delta_min, altitude_delta_max): - # Generate min and max disp grids from dems - disp_range_grid = ( - self.dense_matching_app.generate_disparity_grids( - pairs[pair_key]["sensor_image_right"], - pairs[pair_key]["corrected_grid_right"], - self.geom_plugin_with_dem_and_geoid, - dem_min=dem_min, - dem_max=dem_max, - dem_median=dem_median, - pair_folder=dense_matching_pair_folder, - ) - ) - else: - # Generate min and max disp grids from deltas - disp_range_grid = ( - self.dense_matching_app.generate_disparity_grids( - pairs[pair_key]["sensor_image_right"], - pairs[pair_key]["corrected_grid_right"], - self.geom_plugin_with_dem_and_geoid, - altitude_delta_min=altitude_delta_min, - altitude_delta_max=altitude_delta_max, - dem_median=dem_median, - pair_folder=dense_matching_pair_folder, - ) - ) - - # Get margins used in dense matching, - dense_matching_margins_fun = ( - self.dense_matching_app.get_margins_fun( - pairs[pair_key]["corrected_grid_left"], disp_range_grid - ) - ) - - # TODO add in metadata.json max diff max - min - # Update used_conf configuration with epipolar a priori - # Add global min and max computed with grids - advanced_parameters.update_conf( - self.used_conf, - dmin=np.min( - disp_range_grid[0, 0]["disp_min_grid"].values - ), # TODO compute dmin dans dmax - dmax=np.max(disp_range_grid[0, 0]["disp_max_grid"].values), - pair_key=pair_key, - ) - # saved used configuration - cars_dataset.save_dict( - self.used_conf, - os.path.join(out_dir, "used_conf.json"), - safe_save=True, - ) - - # Generate roi - epipolar_roi = preprocessing.compute_epipolar_roi( - self.input_roi_poly, - self.input_roi_epsg, - self.geom_plugin_with_dem_and_geoid, - pairs[pair_key]["sensor_image_left"], - pairs[pair_key]["sensor_image_right"], - pairs[pair_key]["corrected_grid_left"], - pairs[pair_key]["corrected_grid_right"], - os.path.join(dump_dir, "compute_epipolar_roi", pair_key), - disp_min=np.min( - disp_range_grid[0, 0]["disp_min_grid"].values - ), # TODO compute dmin dans dmax - disp_max=np.max( - disp_range_grid[0, 0]["disp_max_grid"].values - ), - ) - - # Generate new epipolar images - # Generated with corrected grids - # Optimal size is computed for the worst case scenario - # found with epipolar disparity range grids - - ( - optimum_tile_size, - local_tile_optimal_size_fun, - ) = self.dense_matching_app.get_optimal_tile_size( - disp_range_grid, - cars_orchestrator.cluster.checked_conf_cluster[ - "max_ram_per_worker" - ], - ) - ( - new_epipolar_image_left, - new_epipolar_image_right, - ) = self.resampling_application.run( - pairs[pair_key]["sensor_image_left"], - pairs[pair_key]["sensor_image_right"], - pairs[pair_key]["corrected_grid_left"], - pairs[pair_key]["corrected_grid_right"], - orchestrator=cars_orchestrator, - pair_folder=os.path.join( - dump_dir, "resampling", "corrected_grid", pair_key - ), - pair_key=pair_key, - margins_fun=dense_matching_margins_fun, - tile_width=optimum_tile_size, - tile_height=optimum_tile_size, - add_color=True, - add_classif=True, - epipolar_roi=epipolar_roi, - ) - - # Run epipolar matching application - epipolar_disparity_map = self.dense_matching_app.run( - new_epipolar_image_left, - new_epipolar_image_right, - local_tile_optimal_size_fun, - orchestrator=cars_orchestrator, - pair_folder=os.path.join( - dump_dir, "dense_matching", pair_key - ), - pair_key=pair_key, - disp_range_grid=disp_range_grid, - compute_disparity_masks=False, - disp_to_alt_ratio=pairs[pair_key][ - "corrected_grid_left" - ].attributes["disp_to_alt_ratio"], - ) - - # Dense matches filling - if self.dense_matches_filling_1.used_method == "plane": - # Fill holes in disparity map - (filled_with_1_epipolar_disparity_map) = ( - self.dense_matches_filling_1.run( - epipolar_disparity_map, - pairs[pair_key]["holes_bbox_left"], - pairs[pair_key]["holes_bbox_right"], - disp_min=np.min( - disp_range_grid[0, 0]["disp_min_grid"].values - ), - disp_max=np.max( - disp_range_grid[0, 0]["disp_max_grid"].values - ), - orchestrator=cars_orchestrator, - pair_folder=os.path.join( - dump_dir, "dense_matches_filling_1", pair_key - ), - pair_key=pair_key, - ) - ) - else: - # Fill with zeros - (filled_with_1_epipolar_disparity_map) = ( - self.dense_matches_filling_1.run( - epipolar_disparity_map, - orchestrator=cars_orchestrator, - pair_folder=os.path.join( - dump_dir, "dense_matches_filling_1", pair_key - ), - pair_key=pair_key, - ) - ) - - if self.dense_matches_filling_2.used_method == "plane": - # Fill holes in disparity map - (filled_with_2_epipolar_disparity_map) = ( - self.dense_matches_filling_2.run( - filled_with_1_epipolar_disparity_map, - pairs[pair_key]["holes_bbox_left"], - pairs[pair_key]["holes_bbox_right"], - disp_min=np.min( - disp_range_grid[0, 0]["disp_min_grid"].values - ), - disp_max=np.max( - disp_range_grid[0, 0]["disp_max_grid"].values - ), - orchestrator=cars_orchestrator, - pair_folder=os.path.join( - dump_dir, "dense_matches_filling_2", pair_key - ), - pair_key=pair_key, - ) - ) - else: - # Fill with zeros - (filled_with_2_epipolar_disparity_map) = ( - self.dense_matches_filling_2.run( - filled_with_1_epipolar_disparity_map, - orchestrator=cars_orchestrator, - pair_folder=os.path.join( - dump_dir, "dense_matches_filling_2", pair_key - ), - pair_key=pair_key, - ) - ) - - if epsg is None: - # compute epsg - # Epsg uses global disparity min and max - epsg = preprocessing.compute_epsg( - pairs[pair_key]["sensor_image_left"], - pairs[pair_key]["sensor_image_right"], - pairs[pair_key]["corrected_grid_left"], - pairs[pair_key]["corrected_grid_right"], - self.geom_plugin_with_dem_and_geoid, - disp_min=np.min( - disp_range_grid[0, 0]["disp_min_grid"].values - ), - disp_max=np.max( - disp_range_grid[0, 0]["disp_max_grid"].values - ), - ) - # Compute roi polygon, in input EPSG - roi_poly = preprocessing.compute_roi_poly( - self.input_roi_poly, self.input_roi_epsg, epsg - ) - - # Checking disparity intervals indicators - if self.application_conf["dense_matching"][ - "generate_confidence_intervals" - ]: - intervals = [cst_disp.INTERVAL_INF, cst_disp.INTERVAL_SUP] - intervals_pair_flag = False - for key, item in self.dense_matching_app.corr_config[ - "pipeline" - ].items(): - if ( - cst_disp.CONFIDENCE_KEY in key - and item["confidence_method"] == cst_disp.INTERVAL - ): - indicator = key.split(".") - if not intervals_pair_flag: - if len(indicator) > 1: - intervals[0] += "." + indicator[-1] - intervals[1] += "." + indicator[-1] - # Only processing the first encountered interval - intervals_pair_flag = True - else: - warn_msg = ( - "Multiple confidence intervals " - "is not supported. {} will be " - "ignored. Only {} will be processed" - ).format(key, intervals) - logging.warning(warn_msg) - else: - intervals = None - - if isinstance(self.output[sens_cst.GEOID], str): - output_geoid_path = self.output[sens_cst.GEOID] - elif ( - isinstance(self.output[sens_cst.GEOID], bool) - and self.output[sens_cst.GEOID] - ): - package_path = os.path.dirname(__file__) - output_geoid_path = os.path.join( - package_path, - "..", - "..", - "conf", - sensor_inputs.CARS_GEOID_PATH, - ) - else: - # default case : stay on the ellipsoid - output_geoid_path = None - - depth_map_dir = None - if self.save_output_depth_map: - depth_map_dir = os.path.join(out_dir, "depth_map", pair_key) - safe_makedirs(depth_map_dir) - - # Run epipolar triangulation application - epipolar_points_cloud = self.triangulation_application.run( - pairs[pair_key]["sensor_image_left"], - pairs[pair_key]["sensor_image_right"], - new_epipolar_image_left, - pairs[pair_key]["corrected_grid_left"], - pairs[pair_key]["corrected_grid_right"], - filled_with_2_epipolar_disparity_map, - epsg, - self.geom_plugin_without_dem_and_geoid, - denoising_overload_fun=denoising_overload_fun, - source_pc_names=pairs_names, - orchestrator=cars_orchestrator, - pair_dump_dir=os.path.join( - dump_dir, "triangulation", pair_key - ), - pair_key=pair_key, - uncorrected_grid_right=pairs[pair_key]["grid_right"], - geoid_path=output_geoid_path, - cloud_id=cloud_id, - intervals=intervals, - pair_output_dir=depth_map_dir, - save_output_color=bool(depth_map_dir) - and auxiliary[output_constants.AUX_COLOR], - save_output_classification=bool(depth_map_dir) - and auxiliary[output_constants.AUX_CLASSIFICATION], - save_output_filling=bool(depth_map_dir) - and auxiliary[output_constants.AUX_FILLING], - save_output_mask=bool(depth_map_dir) - and auxiliary[output_constants.AUX_MASK], - save_output_performance_map=bool(depth_map_dir) - and auxiliary[output_constants.AUX_PERFORMANCE_MAP], - ) - - if "no_merging" in self.used_conf[PIPELINE]: - denoised_epipolar_points_cloud = ( - self.pc_denoising_application.run( - epipolar_points_cloud, - orchestrator=cars_orchestrator, - pair_folder=os.path.join( - dump_dir, "denoising", pair_key - ), - pair_key=pair_key, - ) - ) - - if self.generate_terrain_products: - # Compute terrain bounding box /roi related to - # current images - (current_terrain_roi_bbox) = ( - preprocessing.compute_terrain_bbox( - pairs[pair_key]["sensor_image_left"], - pairs[pair_key]["sensor_image_right"], - new_epipolar_image_left, - pairs[pair_key]["corrected_grid_left"], - pairs[pair_key]["corrected_grid_right"], - epsg, - self.geom_plugin_with_dem_and_geoid, - resolution=resolution, - disp_min=np.min( - disp_range_grid[0, 0]["disp_min_grid"].values - ), - disp_max=np.max( - disp_range_grid[0, 0]["disp_max_grid"].values - ), - roi_poly=( - None if self.debug_with_roi else roi_poly - ), - orchestrator=cars_orchestrator, - pair_key=pair_key, - pair_folder=os.path.join( - dump_dir, "terrain_bbox", pair_key - ), - check_inputs=True, - ) - ) - list_terrain_roi.append(current_terrain_roi_bbox) - - # add points cloud to list - if "no_merging" in self.used_conf[PIPELINE]: - list_epipolar_points_cloud.append( - denoised_epipolar_points_cloud - ) - else: - list_epipolar_points_cloud.append(epipolar_points_cloud) - - if self.generate_terrain_products: - # compute terrain bounds - ( - terrain_bounds, - optimal_terrain_tile_width, - ) = preprocessing.compute_terrain_bounds( - list_terrain_roi, - roi_poly=(None if self.debug_with_roi else roi_poly), - resolution=resolution, - ) - - if "no_merging" in self.used_conf[PIPELINE]: - point_cloud_to_rasterize = ( - list_epipolar_points_cloud, - terrain_bounds, - ) - else: - # Merge point clouds - pc_outliers_removing_1_margins = ( - self.pc_outliers_removing_1_app.get_on_ground_margin( - resolution=resolution - ) - ) - pc_outliers_removing_2_margins = ( - self.pc_outliers_removing_2_app.get_on_ground_margin( - resolution=resolution - ) - ) - - # find which application produce the final version of the - # point cloud. The last generated point cloud will be saved - # as official point cloud product if save_output_point_cloud - # is True. - - last_pc_application = None - # denoising application will produce a point cloud, unless - # it uses the 'none' method. - if self.pc_denoising_application.used_method != "none": - last_pc_application = "denoising" - elif ( - self.pc_outliers_removing_2_app.used_config.get( - "activated", False - ) - is True - ): - last_pc_application = "pc_outliers_removing_2" - elif ( - self.pc_outliers_removing_1_app.used_config.get( - "activated", False - ) - is True - ): - last_pc_application = "pc_outliers_removing_1" - else: - last_pc_application = "fusion" - - merged_points_clouds = self.pc_fusion_application.run( - list_epipolar_points_cloud, - terrain_bounds, - epsg, - source_pc_names=pairs_names, - orchestrator=cars_orchestrator, - margins=( - pc_outliers_removing_1_margins - + pc_outliers_removing_2_margins - + self.rasterization_application.get_margins( - resolution - ) - ), - optimal_terrain_tile_width=optimal_terrain_tile_width, - roi=(roi_poly if self.debug_with_roi else None), - save_laz_output=self.save_output_point_cloud - and last_pc_application == "fusion", - ) - - # Remove outliers with small components method - filtered_1_merged_points_clouds = ( - self.pc_outliers_removing_1_app.run( - merged_points_clouds, - orchestrator=cars_orchestrator, - save_laz_output=self.save_output_point_cloud - and last_pc_application == "pc_outliers_removing_1", - ) - ) - - # Remove outliers with statistical components method - filtered_2_merged_points_clouds = ( - self.pc_outliers_removing_2_app.run( - filtered_1_merged_points_clouds, - orchestrator=cars_orchestrator, - save_laz_output=self.save_output_point_cloud - and last_pc_application == "pc_outliers_removing_2", - ) - ) - - # denoise point cloud - denoised_merged_points_clouds = ( - self.pc_denoising_application.run( - filtered_2_merged_points_clouds, - orchestrator=cars_orchestrator, - save_laz_output=self.save_output_point_cloud - and last_pc_application == "denoising", - ) - ) - - # Rasterize merged and filtered point cloud - point_cloud_to_rasterize = denoised_merged_points_clouds - - rasterization_dump_dir = os.path.join(dump_dir, "rasterization") - - dsm_file_name = ( - os.path.join( - out_dir, - output_constants.DSM_DIRECTORY, - "dsm.tif", - ) - if self.save_output_dsm - else None - ) - - color_file_name = ( - os.path.join( - out_dir, - output_constants.DSM_DIRECTORY, - "color.tif", - ) - if self.save_output_dsm - and self.output[output_constants.AUXILIARY][ - output_constants.AUX_COLOR - ] - else None - ) - - performance_map_file_name = ( - os.path.join( - out_dir, - output_constants.DSM_DIRECTORY, - "performance_map.tif", - ) - if self.save_output_dsm - and self.output[output_constants.AUXILIARY][ - output_constants.AUX_PERFORMANCE_MAP - ] - else None - ) - - classif_file_name = ( - os.path.join( - out_dir, - output_constants.DSM_DIRECTORY, - "classification.tif", - ) - if self.save_output_dsm - and self.output[output_constants.AUXILIARY][ - output_constants.AUX_CLASSIFICATION - ] - else None - ) - - mask_file_name = ( - os.path.join( - out_dir, - output_constants.DSM_DIRECTORY, - "mask.tif", - ) - if self.save_output_dsm - and self.output[output_constants.AUXILIARY][ - output_constants.AUX_MASK - ] - else None - ) - - contributing_pair_file_name = ( - os.path.join( - out_dir, - output_constants.DSM_DIRECTORY, - "contributing_pair.tif", - ) - if self.save_output_dsm - and self.output[output_constants.AUXILIARY][ - output_constants.AUX_CONTRIBUTING_PAIR - ] - else None - ) - - filling_file_name = ( - os.path.join( - out_dir, - output_constants.DSM_DIRECTORY, - "filling.tif", - ) - if self.save_output_dsm - and self.output[output_constants.AUXILIARY][ - output_constants.AUX_FILLING - ] - else None - ) - - # rasterize point cloud - _ = self.rasterization_application.run( - point_cloud_to_rasterize, - epsg, - resolution=resolution, - orchestrator=cars_orchestrator, - dsm_file_name=dsm_file_name, - color_file_name=color_file_name, - classif_file_name=classif_file_name, - performance_map_file_name=performance_map_file_name, - mask_file_name=mask_file_name, - contributing_pair_file_name=contributing_pair_file_name, - filling_file_name=filling_file_name, - color_dtype=list_epipolar_points_cloud[0].attributes[ - "color_type" - ], - dump_dir=rasterization_dump_dir, - ) - - # Cleaning: don't keep terrain bbox if save_intermediate_data - # is not activated - if not self.advanced[adv_cst.SAVE_INTERMEDIATE_DATA]: - cars_orchestrator.add_to_clean( - os.path.join(dump_dir, "terrain_bbox") - ) - - # Cleaning: delete everything in tile_processing if - # save_intermediate_data is not activated - if not self.advanced[adv_cst.SAVE_INTERMEDIATE_DATA]: - cars_orchestrator.add_to_clean( - os.path.join(dump_dir, "tile_processing") - ) diff --git a/cars/pipelines/sensor_to_sparse_dsm/__init__.py b/cars/pipelines/sensor_to_sparse_dsm/__init__.py deleted file mode 100644 index 0d82af2a..00000000 --- a/cars/pipelines/sensor_to_sparse_dsm/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python -# coding: utf8 -# -# Copyright (c) 2020 Centre National d'Etudes Spatiales (CNES). -# -# This file is part of CARS -# (see https://github.com/CNES/cars). -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -""" -CARS sensor_to_dense_dsm pipeline module init file -""" - -# Cars imports -from cars.pipelines.sensor_to_sparse_dsm import ( # noqa: F401 - sensor_to_sparse_dsm_pipeline, -) diff --git a/cars/pipelines/sensor_to_sparse_dsm/sensor_to_sparse_dsm_pipeline.py b/cars/pipelines/sensor_to_sparse_dsm/sensor_to_sparse_dsm_pipeline.py deleted file mode 100644 index 03d63bff..00000000 --- a/cars/pipelines/sensor_to_sparse_dsm/sensor_to_sparse_dsm_pipeline.py +++ /dev/null @@ -1,642 +0,0 @@ -#!/usr/bin/env python -# coding: utf8 -# -# Copyright (c) 2020 Centre National d'Etudes Spatiales (CNES). -# -# This file is part of CARS -# (see https://github.com/CNES/cars). -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -""" -CARS sensors_to_sparse_dsm pipeline class file -""" - -# Standard imports -from __future__ import print_function - -import copy -import json -import logging -import os - -# CARS imports -from cars import __version__ -from cars.applications.application import Application -from cars.applications.dem_generation import ( - dem_generation_constants as dem_gen_cst, -) -from cars.applications.dem_generation import dem_generation_tools -from cars.applications.grid_generation import grid_correction -from cars.applications.sparse_matching import sparse_matching_tools -from cars.core import roi_tools -from cars.core.geometry.abstract_geometry import AbstractGeometry -from cars.core.utils import safe_makedirs -from cars.data_structures import cars_dataset -from cars.orchestrator import orchestrator -from cars.orchestrator.cluster.log_wrapper import cars_profile -from cars.pipelines.parameters import advanced_parameters -from cars.pipelines.parameters import advanced_parameters_constants as adv_cst -from cars.pipelines.parameters import ( - output_constants, - output_parameters, - sensor_inputs, -) -from cars.pipelines.parameters import sensor_inputs_constants as sens_cst -from cars.pipelines.pipeline import Pipeline -from cars.pipelines.pipeline_constants import ( - ADVANCED, - APPLICATIONS, - GEOMETRY_PLUGIN, - INPUTS, - ORCHESTRATOR, - OUTPUT, - PIPELINE, -) -from cars.pipelines.pipeline_template import PipelineTemplate - -# Path in cars package (pkg) -CARS_GEOID_PATH = "geoid/egm96.grd" - - -@Pipeline.register("sensors_to_sparse_dsm") -class SensorSparseDsmPipeline(PipelineTemplate): - """ - SensorSparseDsmPipeline - """ - - # pylint: disable=too-many-instance-attributes - - def __init__(self, conf, config_json_dir=None): - """ - Creates pipeline - - :param pipeline_name: name of the pipeline. - :type pipeline_name: str - :param cfg: configuration {'matching_cost_method': value} - :type cfg: dictionary - :param config_json_dir: path to dir containing json - :type config_json_dir: str - """ - - # Merge parameters from associated json - # priority : cars_pipeline.json << user_inputs.json - # Get root package directory - package_path = os.path.dirname(__file__) - json_file = os.path.join( - package_path, - "..", - "conf_pipeline", - "sensor_to_sparse_dsm.json", - ) - with open(json_file, "r", encoding="utf8") as fstream: - pipeline_config = json.load(fstream) - - self.conf = self.merge_pipeline_conf(pipeline_config, conf) - - # check global conf - self.check_global_schema(self.conf) - - # Used conf - self.used_conf = {} - - # Prepared config full res - self.config_full_res = {} - - # Pipeline - self.used_conf[PIPELINE] = "sensors_to_sparse_dsm" - - # Check conf orchestrator - self.orchestrator_conf = self.check_orchestrator( - self.conf.get(ORCHESTRATOR, None) - ) - self.used_conf[ORCHESTRATOR] = self.orchestrator_conf - - # Check conf inputs - self.inputs = self.check_inputs( - self.conf[INPUTS], config_json_dir=config_json_dir - ) - - # Check advanced parameters - # TODO static method in the base class - self.advanced = advanced_parameters.check_advanced_parameters( - self.conf.get(ADVANCED, {}), check_epipolar_a_priori=False - ) - self.used_conf[ADVANCED] = self.advanced - - # Check geometry plugin - ( - self.inputs, - self.used_conf[GEOMETRY_PLUGIN], - self.geom_plugin_without_dem_and_geoid, - self.geom_plugin_with_dem_and_geoid, - self.dem_generation_roi, - ) = sensor_inputs.check_geometry_plugin( - self.inputs, self.advanced, self.conf.get(GEOMETRY_PLUGIN, None) - ) - self.used_conf[INPUTS] = self.inputs - - # Get ROI - ( - self.input_roi_poly, - self.input_roi_epsg, - ) = roi_tools.generate_roi_poly_from_inputs( - self.used_conf[INPUTS][sens_cst.ROI] - ) - - # Check conf output - self.output = self.check_output( - self.conf[OUTPUT], self.used_conf[PIPELINE] - ) - self.used_conf[OUTPUT] = self.output - - # Check conf application - application_conf = self.check_applications( - self.conf.get(APPLICATIONS, {}), - save_all_intermediate_data=self.used_conf[ADVANCED][ - adv_cst.SAVE_INTERMEDIATE_DATA - ], - ) - self.used_conf[APPLICATIONS] = application_conf - - self.config_full_res = copy.deepcopy(self.used_conf) - self.config_full_res[PIPELINE] = "sensors_to_dense_dsm" - self.config_full_res.__delitem__("applications") - self.config_full_res[ADVANCED][adv_cst.EPIPOLAR_A_PRIORI] = {} - self.config_full_res[ADVANCED][adv_cst.TERRAIN_A_PRIORI] = {} - self.config_full_res[ADVANCED][adv_cst.USE_EPIPOLAR_A_PRIORI] = True - - def check_inputs(self, conf, config_json_dir=None): - """ - Check the inputs given - - :param conf: configuration of inputs - :type conf: dict - :param config_json_dir: directory of used json, if - user filled paths with relative paths - :type config_json_dir: str - - :return: overloader inputs - :rtype: dict - """ - return sensor_inputs.sensors_check_inputs( - conf, config_json_dir=config_json_dir - ) - - def check_output(self, conf, pipeline): - """ - Check the output given - - :param conf: configuration of output - :type conf: dict - :param pipeline: name of corresponding pipeline - :type pipeline_name: str - - :return: overloader output - :rtype: dict - """ - return output_parameters.check_output_parameters(conf, pipeline) - - def check_applications(self, conf, save_all_intermediate_data=False): - """ - Check the given configuration for applications - - :param conf: configuration of applications - :type conf: dict - :param save_all_intermediate_data: True to save intermediate data in all - applications - :type save_all_intermediate_data: bool - """ - - # Check if all specified applications are used - needed_applications = [ - "grid_generation", - "sparse_matching", - "resampling", - "dem_generation", - ] - - for app_key in conf.keys(): - if app_key not in needed_applications: - logging.error( - "No {} application used in pipeline".format(app_key) - ) - raise NameError( - "No {} application used in pipeline".format(app_key) - ) - - # Initialize used config - used_conf = {} - for app_key in needed_applications: - used_conf[app_key] = conf.get(app_key, {}) - used_conf[app_key]["save_intermediate_data"] = used_conf[ - app_key - ].get("save_intermediate_data", save_all_intermediate_data) - - # Epipolar grid generation - self.epipolar_grid_generation_app = Application( - "grid_generation", cfg=used_conf.get("grid_generation", {}) - ) - used_conf["grid_generation"] = ( - self.epipolar_grid_generation_app.get_conf() - ) - - # Sparse Matching - self.sparse_matching_app = Application( - "sparse_matching", cfg=used_conf.get("sparse_matching", {}) - ) - used_conf["sparse_matching"] = self.sparse_matching_app.get_conf() - - # image resampling - self.resampling_application = Application( - "resampling", cfg=used_conf.get("resampling", {}) - ) - used_conf["resampling"] = self.resampling_application.get_conf() - - # MNT generation - self.dem_generation_application = Application( - "dem_generation", cfg=used_conf.get("dem_generation", {}) - ) - used_conf["dem_generation"] = self.dem_generation_application.get_conf() - - return used_conf - - @cars_profile(name="run_sparse_pipeline", interval=0.5) - def run(self): - """ - Run pipeline - - """ - out_dir = self.output[output_constants.OUT_DIRECTORY] - - # Save used conf - cars_dataset.save_dict( - self.used_conf, - os.path.join(out_dir, "used_conf.json"), - safe_save=True, - ) - - # start cars orchestrator - with orchestrator.Orchestrator( - orchestrator_conf=self.orchestrator_conf, - out_dir=out_dir, - out_json_path=os.path.join( - out_dir, - output_constants.INFO_FILENAME, - ), - ) as cars_orchestrator: - # initialize out_json - cars_orchestrator.update_out_info( - { - "version": __version__, - "pipeline": "sensors_to_sparse_dsm_pipeline", - "inputs": self.inputs, - } - ) - - # Application dump directory - dump_dir = os.path.join(cars_orchestrator.out_dir, "dump_dir") - - # Run applications - list_sensor_pairs = sensor_inputs.generate_inputs( - self.inputs, self.geom_plugin_without_dem_and_geoid - ) - logging.info( - "Received {} stereo pairs configurations".format( - len(list_sensor_pairs) - ) - ) - - pairs = {} - triangulated_matches_list = [] - - for ( - pair_key, - sensor_image_left, - sensor_image_right, - ) in list_sensor_pairs: - pairs[pair_key] = {} - pairs[pair_key]["sensor_image_left"] = sensor_image_left - pairs[pair_key]["sensor_image_right"] = sensor_image_right - - # Run applications - - # Run grid generation - if self.inputs[sens_cst.INITIAL_ELEVATION] is None: - geom_plugin = self.geom_plugin_without_dem_and_geoid - else: - geom_plugin = self.geom_plugin_with_dem_and_geoid - - ( - pairs[pair_key]["grid_left"], - pairs[pair_key]["grid_right"], - ) = self.epipolar_grid_generation_app.run( - pairs[pair_key]["sensor_image_left"], - pairs[pair_key]["sensor_image_right"], - geom_plugin, - orchestrator=cars_orchestrator, - pair_folder=os.path.join( - dump_dir, - "epipolar_grid_generation", - "initial", - pair_key, - ), - pair_key=pair_key, - ) - - # Run epipolar resampling - ( - pairs[pair_key]["epipolar_image_left"], - pairs[pair_key]["epipolar_image_right"], - ) = self.resampling_application.run( - pairs[pair_key]["sensor_image_left"], - pairs[pair_key]["sensor_image_right"], - pairs[pair_key]["grid_left"], - pairs[pair_key]["grid_right"], - orchestrator=cars_orchestrator, - pair_folder=os.path.join(dump_dir, "resampling", pair_key), - pair_key=pair_key, - margins_fun=self.sparse_matching_app.get_margins_fun(), - tile_width=None, - tile_height=None, - add_color=False, - add_classif=False, - ) - - # Run epipolar sparse_matching application - ( - pairs[pair_key]["epipolar_matches_left"], - _, - ) = self.sparse_matching_app.run( - pairs[pair_key]["epipolar_image_left"], - pairs[pair_key]["epipolar_image_right"], - pairs[pair_key]["grid_left"].attributes[ - "disp_to_alt_ratio" - ], - orchestrator=cars_orchestrator, - pair_folder=os.path.join( - dump_dir, "sparse_matching", pair_key - ), - pair_key=pair_key, - ) - - # Run cluster breakpoint to compute sifts - cars_orchestrator.breakpoint() - - # Run grid correction application - - # Filter matches - matches_array = self.sparse_matching_app.filter_matches( - pairs[pair_key]["epipolar_matches_left"], - pairs[pair_key]["grid_left"], - pairs[pair_key]["grid_right"], - orchestrator=cars_orchestrator, - pair_key=pair_key, - pair_folder=os.path.join( - dump_dir, "sparse_matching", pair_key - ), - save_matches=self.sparse_matching_app.get_save_matches(), - ) - # Estimate grid correction - ( - pairs[pair_key]["grid_correction_coef"], - pairs[pair_key]["corrected_matches_array"], - pairs[pair_key]["corrected_matches_cars_ds"], - _, - _, - ) = grid_correction.estimate_right_grid_correction( - matches_array, - pairs[pair_key]["grid_right"], - initial_cars_ds=pairs[pair_key]["epipolar_matches_left"], - save_matches=self.sparse_matching_app.get_save_matches(), - pair_folder=os.path.join( - dump_dir, "grid_correction", "initial", pair_key - ), - pair_key=pair_key, - orchestrator=cars_orchestrator, - ) - - # Correct grid right - pairs[pair_key]["corrected_grid_right"] = ( - grid_correction.correct_grid( - pairs[pair_key]["grid_right"], - pairs[pair_key]["grid_correction_coef"], - self.epipolar_grid_generation_app.get_save_grids(), - os.path.join( - dump_dir, "grid_correction", "initial", pair_key - ), - ) - ) - - pairs[pair_key]["corrected_grid_left"] = pairs[pair_key][ - "grid_left" - ] - - # Triangulate matches - pairs[pair_key]["triangulated_matches"] = ( - dem_generation_tools.triangulate_sparse_matches( - pairs[pair_key]["sensor_image_left"], - pairs[pair_key]["sensor_image_right"], - pairs[pair_key]["grid_left"], - pairs[pair_key]["corrected_grid_right"], - pairs[pair_key]["corrected_matches_array"], - geom_plugin, - ) - ) - # filter triangulated_matches - matches_filter_knn = ( - self.sparse_matching_app.get_matches_filter_knn() - ) - matches_filter_dev_factor = ( - self.sparse_matching_app.get_matches_filter_dev_factor() - ) - pairs[pair_key]["filtered_triangulated_matches"] = ( - sparse_matching_tools.filter_point_cloud_matches( - pairs[pair_key]["triangulated_matches"], - matches_filter_knn=matches_filter_knn, - matches_filter_dev_factor=matches_filter_dev_factor, - ) - ) - triangulated_matches_list.append( - pairs[pair_key]["filtered_triangulated_matches"] - ) - - dem_generation_output_dir = os.path.join( - cars_orchestrator.out_dir, "dump_dir", "dem_generation" - ) - safe_makedirs(dem_generation_output_dir) - - # Generate MNT from matches - dem = self.dem_generation_application.run( - triangulated_matches_list, - dem_generation_output_dir, - self.inputs[sens_cst.INITIAL_ELEVATION][sens_cst.GEOID], - dem_roi_to_use=self.dem_generation_roi, - ) - dem_median = dem.attributes[dem_gen_cst.DEM_MEDIAN_PATH] - # Generate geometry loader with dem and geoid - self.geom_plugin_with_dem_and_geoid = ( - sensor_inputs.generate_geometry_plugin_with_dem( - self.used_conf[GEOMETRY_PLUGIN], - self.inputs, - dem=dem_median, - ) - ) - dem_min = dem.attributes[dem_gen_cst.DEM_MIN_PATH] - dem_max = dem.attributes[dem_gen_cst.DEM_MAX_PATH] - - # Generate geometry loader with dem and geoid - self.geom_plugin_with_dem_and_geoid = ( - sensor_inputs.generate_geometry_plugin_with_dem( - self.used_conf[GEOMETRY_PLUGIN], - self.inputs, - dem=dem_median, - ) - ) - - advanced_parameters.update_conf( - self.config_full_res, - dem_median=dem_median, - dem_min=dem_min, - dem_max=dem_max, - ) - - for pair_key, _, _ in list_sensor_pairs: - geom_plugin = self.geom_plugin_with_dem_and_geoid - # Generate grids with new MNT - ( - pairs[pair_key]["new_grid_left"], - pairs[pair_key]["new_grid_right"], - ) = self.epipolar_grid_generation_app.run( - pairs[pair_key]["sensor_image_left"], - pairs[pair_key]["sensor_image_right"], - geom_plugin, - orchestrator=cars_orchestrator, - pair_folder=os.path.join( - dump_dir, - "epipolar_grid_generation", - "new_mnt", - pair_key, - ), - pair_key=pair_key, - ) - - # Correct grids with former matches - # Transform matches to new grids - - new_grid_matches_array = ( - AbstractGeometry.transform_matches_from_grids( - pairs[pair_key]["corrected_matches_array"], - pairs[pair_key]["corrected_grid_left"], - pairs[pair_key]["corrected_grid_right"], - pairs[pair_key]["new_grid_left"], - pairs[pair_key]["new_grid_right"], - ) - ) - - # Estimate grid_correction - ( - pairs[pair_key]["grid_correction_coef"], - corrected_matches_array, - pairs[pair_key]["corrected_matches_cars_ds"], - _, - _, - ) = grid_correction.estimate_right_grid_correction( - new_grid_matches_array, - pairs[pair_key]["new_grid_right"], - initial_cars_ds=pairs[pair_key]["epipolar_matches_left"], - save_matches=(self.sparse_matching_app.get_save_matches()), - pair_folder=os.path.join( - dump_dir, - "grid_correction", - pair_key, - ), - pair_key=pair_key, - orchestrator=cars_orchestrator, - ) - - # Correct grid right - pairs[pair_key]["corrected_grid_right"] = ( - grid_correction.correct_grid( - pairs[pair_key]["new_grid_right"], - pairs[pair_key]["grid_correction_coef"], - self.epipolar_grid_generation_app.get_save_grids(), - pair_folder=os.path.join( - dump_dir, - "grid_correction", - pair_key, - ), - ) - ) - pairs[pair_key]["corrected_grid_left"] = pairs[pair_key][ - "new_grid_left" - ] - - # Triangulate new matches - pairs[pair_key]["triangulated_matches"] = ( - dem_generation_tools.triangulate_sparse_matches( - pairs[pair_key]["sensor_image_left"], - pairs[pair_key]["sensor_image_right"], - pairs[pair_key]["corrected_grid_left"], - pairs[pair_key]["corrected_grid_right"], - corrected_matches_array, - geometry_plugin=geom_plugin, - ) - ) - - # filter triangulated_matches - matches_filter_knn = ( - self.sparse_matching_app.get_matches_filter_knn() - ) - matches_filter_dev_factor = ( - self.sparse_matching_app.get_matches_filter_dev_factor() - ) - pairs[pair_key]["filtered_triangulated_matches"] = ( - sparse_matching_tools.filter_point_cloud_matches( - pairs[pair_key]["triangulated_matches"], - matches_filter_knn=matches_filter_knn, - matches_filter_dev_factor=matches_filter_dev_factor, - ) - ) - - # Compute disp_min and disp_max - (dmin, dmax) = sparse_matching_tools.compute_disp_min_disp_max( - pairs[pair_key]["filtered_triangulated_matches"], - cars_orchestrator, - disp_margin=( - self.sparse_matching_app.get_disparity_margin() - ), - pair_key=pair_key, - disp_to_alt_ratio=pairs[pair_key][ - "corrected_grid_left" - ].attributes["disp_to_alt_ratio"], - ) - - # Update full res pipeline configuration - # with grid correction and disparity range - advanced_parameters.update_conf( - self.config_full_res, - grid_correction_coef=pairs[pair_key][ - "grid_correction_coef" - ], - dmin=dmin, - dmax=dmax, - pair_key=pair_key, - ) - - # Save the refined full res pipeline configuration - cars_dataset.save_dict( - self.config_full_res, - os.path.join(out_dir, "refined_config_dense_dsm.json"), - safe_save=True, - ) diff --git a/docs/source/usage.rst b/docs/source/usage.rst index e6798f57..a2b5af64 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -7,7 +7,7 @@ Usage Command line ============ -``cars`` command line is the entry point for CARS to run 3D pipelines. +``cars`` command line is the entry point for CARS to run the 3D pipeline. .. code-block:: console @@ -67,7 +67,6 @@ The structure follows this organisation: "orchestrator": {}, "applications": {}, "output": {}, - "pipeline": "pipeline_to_use", "geometry_plugin": "geometry_plugin_to_use" } @@ -77,345 +76,338 @@ The structure follows this organisation: .. tabs:: - .. tab:: Inputs + .. tab:: Inputs - Inputs depends on the pipeline used by CARS. CARS can be entered with Sensor Images or Depth Maps: - - * Sensor Images: used in "sensors_to_dense_dsm", "sensors_to_sparse_dsm", "sensors_to_dense_depth_maps" pipelines. - * Depth Maps: used in "dense_depth_maps_to_dense_dsm" pipeline. - - - .. tabs:: - - .. tab:: Sensors Images inputs + CARS can be entered either with Sensor Images or with Depth Maps. + + Additional inputs can be provided for both types of inputs, namely a ROI and an initial elevation. + .. tabs:: + .. tab:: Sensors Images inputs - +----------------------------+---------------------------------------------------------------------+-----------------------+----------------------+----------+ - | Name | Description | Type | Default value | Required | - +============================+=====================================================================+=======================+======================+==========+ - | *sensor* | Stereo sensor images | See next section | No | Yes | - +----------------------------+---------------------------------------------------------------------+-----------------------+----------------------+----------+ - | *pairing* | Association of image to create pairs | list of *sensor* | No | Yes (*) | - +----------------------------+---------------------------------------------------------------------+-----------------------+----------------------+----------+ - | *initial_elevation* | Path to SRTM tiles (see :ref:`plugins` section for details) | string | None | No | - | | If not provided, internal dem is generated with sparse matches | | | | - +----------------------------+---------------------------------------------------------------------+-----------------------+----------------------+----------+ - | *roi* | ROI: Vector file path or GeoJson | string, dict | None | No | - +----------------------------+---------------------------------------------------------------------+-----------------------+----------------------+----------+ + +----------------------------+---------------------------------------------------------------------+-----------------------+----------------------+----------+ + | Name | Description | Type | Default value | Required | + +============================+=====================================================================+=======================+======================+==========+ + | *sensor* | Stereo sensor images | See next section | No | Yes | + +----------------------------+---------------------------------------------------------------------+-----------------------+----------------------+----------+ + | *pairing* | Association of image to create pairs | list of *sensor* | No | Yes (*) | + +----------------------------+---------------------------------------------------------------------+-----------------------+----------------------+----------+ - (*) `pairing` is required If there are more than two sensors (see pairing section below) + (*) `pairing` is required If there are more than two sensors (see pairing section below) - **Sensor** + **Sensor** - For each sensor images, give a particular name (what you want): + For each sensor images, give a particular name (what you want): - .. code-block:: json + .. code-block:: json - { - "my_name_for_this_image": { - "image" : "path_to_image.tif", - "color" : "path_to_color.tif", - "mask" : "path_to_mask.tif", - "classification" : "path_to_classification.tif", - "nodata": 0 + "my_name_for_this_image": + { + "image" : "path_to_image.tif", + "color" : "path_to_color.tif", + "mask" : "path_to_mask.tif", + "classification" : "path_to_classification.tif", + "nodata": 0 + } } - } - - +-------------------+------------------------------------------------------------------------------------------+----------------+---------------+----------+ - | Name | Description | Type | Default value | Required | - +===================+==========================================================================================+================+===============+==========+ - | *image* | Path to the image | string | | Yes | - +-------------------+------------------------------------------------------------------------------------------+----------------+---------------+----------+ - | *color* | Image stackable to image used to create an ortho-image corresponding to the produced dsm | string | | No | - +-------------------+------------------------------------------------------------------------------------------+----------------+---------------+----------+ - | *no_data* | No data value of the image | int | 0 | No | - +-------------------+------------------------------------------------------------------------------------------+----------------+---------------+----------+ - | *geomodel* | Path of geomodel and plugin-specific attributes (see :ref:`plugins` section for details) | string, dict | | No | - +-------------------+------------------------------------------------------------------------------------------+----------------+---------------+----------+ - | *mask* | Binary mask stackable to image: 0 values are considered valid data | string | None | No | - +-------------------+------------------------------------------------------------------------------------------+----------------+---------------+----------+ - | *classification* | Multiband classification image (label keys inside metadata): 1 values = valid data | string | None | No | - +-------------------+------------------------------------------------------------------------------------------+----------------+---------------+----------+ - - .. note:: - - - *color*: This image can be composed of XS bands in which case a PAN+XS fusion has been be performed. Please, see the section :ref:`make_a_simple_pan_sharpening` to make a simple pan sharpening with OTB if necessary. - - If the *classification* configuration file is indicated, all non-zeros values of the classification image will be considered as invalid data. - - Please, see the section :ref:`convert_image_to_binary_image` to make binary mask image or binary classification with 1 bit per band. - - The classification of second input is not necessary. In this case, the applications use only the available classification. - - Please, see the section :ref:`add_band_description_in_image` to add band name / description in order to be used in Applications - - *geomodel*: If no geomodel is provide, CARS will try to use the rpc loaded with rasterio opening *image*. - - **Pairing** - - The pairing attribute defines the pairs to use, using sensors keys used to define sensor images. - - .. code-block:: json - { - "inputs": { - "sensors" : { - "one": { - "image": "img1.tif", - "geomodel": "img1.geom" - }, - "two": { - "image": "img2.tif", - "geomodel": "img2.geom" + +-------------------+------------------------------------------------------------------------------------------+----------------+---------------+----------+ + | Name | Description | Type | Default value | Required | + +===================+==========================================================================================+================+===============+==========+ + | *image* | Path to the image | string | | Yes | + +-------------------+------------------------------------------------------------------------------------------+----------------+---------------+----------+ + | *color* | Image stackable to image used to create an ortho-image corresponding to the produced dsm | string | | No | + +-------------------+------------------------------------------------------------------------------------------+----------------+---------------+----------+ + | *no_data* | No data value of the image | int | 0 | No | + +-------------------+------------------------------------------------------------------------------------------+----------------+---------------+----------+ + | *geomodel* | Path of geomodel and plugin-specific attributes (see :ref:`plugins` section for details) | string, dict | | No | + +-------------------+------------------------------------------------------------------------------------------+----------------+---------------+----------+ + | *mask* | Binary mask stackable to image: 0 values are considered valid data | string | None | No | + +-------------------+------------------------------------------------------------------------------------------+----------------+---------------+----------+ + | *classification* | Multiband classification image (label keys inside metadata): 1 values = valid data | string | None | No | + +-------------------+------------------------------------------------------------------------------------------+----------------+---------------+----------+ + + .. note:: + + - *color*: This image can be composed of XS bands in which case a PAN+XS fusion has been be performed. Please, see the section :ref:`make_a_simple_pan_sharpening` to make a simple pan sharpening with OTB if necessary. + - If the *classification* configuration file is indicated, all non-zeros values of the classification image will be considered as invalid data. + - Please, see the section :ref:`convert_image_to_binary_image` to make binary mask image or binary classification with 1 bit per band. + - The classification of second input is not necessary. In this case, the applications use only the available classification. + - Please, see the section :ref:`add_band_description_in_image` to add band name / description in order to be used in Applications + - *geomodel*: If no geomodel is provide, CARS will try to use the rpc loaded with rasterio opening *image*. + + **Pairing** + + The pairing attribute defines the pairs to use, using sensors keys used to define sensor images. + + .. code-block:: json - }, - "three": { - "image": "img3.tif", - "geomodel": "img3.geom" + { + "inputs": { + "sensors" : { + "one": { + "image": "img1.tif", + "geomodel": "img1.geom" + }, + "two": { + "image": "img2.tif", + "geomodel": "img2.geom" + + }, + "three": { + "image": "img3.tif", + "geomodel": "img3.geom" + } + }, + "pairing": [["one", "two"],["one", "three"]] } - }, - "pairing": [["one", "two"],["one", "three"]] } - } - This attribute is required when there are more than two input sensor images. If only two images ares provided, the pairing can be deduced by cars, considering the first image defined as the left image and second image as right image. + This attribute is required when there are more than two input sensor images. If only two images ares provided, the pairing can be deduced by cars, considering the first image defined as the left image and second image as right image. - **Initial elevation** - The attribute contains all informations about initial elevation: dem path, geoid and default altitude - - +-----------------------+----------------------------------------------------------------------------+--------+----------------------+----------------------------+----------------------------+ - | Name | Description | Type | Available value | Default value | Required | - +=======================+============================================================================+========+======================+============================+============================+ - | *dem* | Path to DEM tiles | string | | None | No | - +-----------------------+----------------------------------------------------------------------------+--------+----------------------+----------------------------+----------------------------+ - | *geoid* | Geoid path | string | | Cars internal geoid | No | - +-----------------------+----------------------------------------------------------------------------+--------+----------------------+----------------------------+----------------------------+ - | *altitude_delta_min* | constant delta in altitude (meters) between dem median and dem min | int | should be > 0 | None | No | - +-----------------------+----------------------------------------------------------------------------+--------+----------------------+----------------------------+----------------------------+ - | *altitude_delta_max* | constant delta in altitude (meters) between dem max and dem median | int | should be > 0 | None | No | - +-----------------------+----------------------------------------------------------------------------+--------+----------------------+----------------------------+----------------------------+ - - If no DEM path is provided, an internal dem is generated with sparse matches. If no geoid is provided, the default cars geoid is used (egm96). If no delta is provided, the dem_min and max generated with sparse matches will be used. - - The Deltas are used following this formula : + .. tab:: Depth Maps inputs - .. code-block:: python + +-------------------------+---------------------------------------------------------------------+-----------------------+----------------------+----------+ + | Name | Description | Type | Default value | Required | + +=========================+=====================================================================+=======================+======================+==========+ + | *depth_maps* | Depth maps to rasterize | dict | No | Yes | + +-------------------------+---------------------------------------------------------------------+-----------------------+----------------------+----------+ + | *roi* | Region Of Interest: Vector file path or GeoJson | string, dict | None | No | + +-------------------------+---------------------------------------------------------------------+-----------------------+----------------------+----------+ - dem_min = initial_elevation - altitude_delta_min - dem_max = initial_elevation + altitude_delta_max - .. warning:: Dem path is mandatory for the use of the altitude deltas. - When there is no DEM data available, a default height above ellipsoid of 0 is used (no coverage for some points or pixels with no_data in the DEM tiles) + **Depth Maps** - Initial elevation can be provided as a dictionary with a field for each parameter, for example: + For each depth map, give a particular name (what you want): + .. code-block:: json - .. code-block:: json - - { - "inputs": { - "initial_elevation": { - "dem": "/path/to/srtm.tif", - "geoid": "/path/to/geoid.tif", - "altitude_delta_min": 10, - "altitude_delta_max": 40 + { + "depth_maps": { + "my_name_for_this_depth_map": + { + "x" : "path_to_x.tif", + "y" : "path_to_y.tif", + "z" : "path_to_z.tif", + "color" : "path_to_color.tif", + "mask": "path_to_mask.tif", + "classification": "path_to_classification.tif", + "filling": "path_to_filling.tif", + "confidence": { + "confidence_name1": "path_to_confidence1.tif", + "confidence_name2": "path_to_confidence2.tif", + }, + "performance_map": "path_to_performance_map.tif", + "epsg": "depth_map_epsg" + } } } - } - - Alternatively, it can be set as a string corresponding to the DEM path, in which case default values for the geoid and the default altitude are used. - .. code-block:: json + These input files can be generated by activating the saving of depth_map using `save_intermediate_data` in the `triangulation` application. + + .. note:: + + To generate confidence maps, the parameter `save_confidence` of `point_cloud_rasterization` should be activated. + + To generate the performance map, the parameters `generate_performance_map` and `save_intermediate_data` of the `dense_matching` application must be activated. + + +------------------+-------------------------------------------------------------------+----------------+---------------+----------+ + | Name | Description | Type | Default value | Required | + +==================+===================================================================+================+===============+==========+ + | *x* | Path to the x coordinates of depth map | string | | Yes | + +------------------+-------------------------------------------------------------------+----------------+---------------+----------+ + | *y* | Path to the y coordinates of depth map | string | | Yes | + +------------------+-------------------------------------------------------------------+----------------+---------------+----------+ + | *z* | Path to the z coordinates of depth map | string | | Yes | + +------------------+-------------------------------------------------------------------+----------------+---------------+----------+ + | *color* | Color of depth map | string | | Yes | + +------------------+-------------------------------------------------------------------+----------------+---------------+----------+ + | *mask* | Validity mask of depth map : 0 values are considered valid data | string | | No | + +------------------+-------------------------------------------------------------------+----------------+---------------+----------+ + | *classification* | Classification of depth map | string | | No | + +------------------+-------------------------------------------------------------------+----------------+---------------+----------+ + | *filling* | Filling map of depth map | string | | No | + +------------------+-------------------------------------------------------------------+----------------+---------------+----------+ + | *confidence* | Dict of paths to the confidences of depth map | dict | | No | + +------------------+-------------------------------------------------------------------+----------------+---------------+----------+ + | *epsg* | Epsg code of depth map | int | 4326 | No | + +------------------+-------------------------------------------------------------------+----------------+---------------+----------+ + + .. tab:: ROI + A terrain ROI can be provided by the user. It can be either a vector file (Shapefile for instance) path, + or a GeoJson dictionary. These structures must contain a single Polygon or MultiPolygon. Multi-features are + not supported. + + Example of the "roi" parameter with a GeoJson dictionnary containing a Polygon as feature : + + .. code-block:: json - { - "inputs": { - "initial_elevation": "/path/to/srtm.tif" + { + "inputs": + { + "roi" : { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "coordinates": [ + [ + [5.194, 44.2064], + [5.194, 44.2059], + [5.195, 44.2059], + [5.195, 44.2064], + [5.194, 44.2064] + ] + ], + "type": "Polygon" + } + } + ] + } + } } - } - - Note that the geoid parameter in initial_elevation is not the geoid used for output products generated after the triangulation step - (see output parameters). - - Elevation management is tightly linked to the geometry plugin used. See :ref:`plugins` section for details - .. tab:: Depth Maps inputs + If the *debug_with_roi* advanced parameter (see dedicated tab) is enabled, the tiling of the entire image is kept but only the tiles intersecting + the ROI are computed. + MultiPolygon feature is only useful if the parameter *debug_with_roi* is activated, otherwise the total footprint of the + MultiPolygon will be used as ROI. - +-------------------------+---------------------------------------------------------------------+-----------------------+----------------------+----------+ - | Name | Description | Type | Default value | Required | - +=========================+=====================================================================+=======================+======================+==========+ - | *depth_maps* | Depth maps to rasterize | dict | No | Yes | - +-------------------------+---------------------------------------------------------------------+-----------------------+----------------------+----------+ - | *roi* | Region Of Interest: Vector file path or GeoJson | string, dict | None | No | - +-------------------------+---------------------------------------------------------------------+-----------------------+----------------------+----------+ + By default epsg 4326 is used. If the user has defined a polygon in a different reference system, the "crs" field must be specified. + Example of the *debug_with_roi* mode utilizing an "roi" parameter of type MultiPolygon as a feature and a specific EPSG. + .. code-block:: json - **Depth Maps** - - For each depth map, give a particular name (what you want): - - .. code-block:: json - - { - "depth_maps": { - "my_name_for_this_depth_map": + { + "inputs": { - "x" : "path_to_x.tif", - "y" : "path_to_y.tif", - "z" : "path_to_z.tif", - "color" : "path_to_color.tif", - "mask": "path_to_mask.tif", - "classification": "path_to_classification.tif", - "filling": "path_to_filling.tif", - "confidence": { - "confidence_name1": "path_to_confidence1.tif", - "confidence_name2": "path_to_confidence2.tif", + "roi" : { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "coordinates": [ + [ + [ + [319700, 3317700], + [319800, 3317700], + [319800, 3317800], + [319800, 3317800], + [319700, 3317700] + ] + ], + [ + [ + [319900, 3317900], + [320000, 3317900], + [320000, 3318000], + [319900, 3318000], + [319900, 3317900] + ] + ] + ], + "type": "MultiPolygon" + } + } + ], + "crs" : + { + "type": "name", + "properties": { + "name": "EPSG:32636" + } + } }, - "performance_map": "path_to_performance_map.tif", - "epsg": "depth_map_epsg" + } + "advanced": + { + "debug_with_roi": true } } - } - These input files can be generated with the `sensors_to_dense_depth_maps` pipeline, or `sensors_to_dense_dsm` pipeline activating the saving of depth_map using `save_intermediate_data` in the `triangulation` application. - - .. note:: - - To generate confidence maps and performance map, parameters `generate_performance_map` and `save_intermediate_data` of `dense_matching` application must be activated in `sensors_to_dense_depth_maps` pipeline. The output performance map is `performance_map.tif`. Then the parameter `save_confidence` of `point_cloud_rasterization` should be activated in dense_depth_maps_to_dense_dsm pipeline to save the performance map. - - +------------------+-------------------------------------------------------------------+----------------+---------------+----------+ - | Name | Description | Type | Default value | Required | - +==================+===================================================================+================+===============+==========+ - | *x* | Path to the x coordinates of depth map | string | | Yes | - +------------------+-------------------------------------------------------------------+----------------+---------------+----------+ - | *y* | Path to the y coordinates of depth map | string | | Yes | - +------------------+-------------------------------------------------------------------+----------------+---------------+----------+ - | *z* | Path to the z coordinates of depth map | string | | Yes | - +------------------+-------------------------------------------------------------------+----------------+---------------+----------+ - | *color* | Color of depth map | string | | Yes | - +------------------+-------------------------------------------------------------------+----------------+---------------+----------+ - | *mask* | Validity mask of depth map : 0 values are considered valid data | string | | No | - +------------------+-------------------------------------------------------------------+----------------+---------------+----------+ - | *classification* | Classification of depth map | string | | No | - +------------------+-------------------------------------------------------------------+----------------+---------------+----------+ - | *filling* | Filling map of depth map | string | | No | - +------------------+-------------------------------------------------------------------+----------------+---------------+----------+ - | *confidence* | Dict of paths to the confidences of depth map | dict | | No | - +------------------+-------------------------------------------------------------------+----------------+---------------+----------+ - | *epsg* | Epsg code of depth map | int | 4326 | No | - +------------------+-------------------------------------------------------------------+----------------+---------------+----------+ - - **Region Of Interest (ROI)** - - A terrain ROI can be provided by user. It can be either a vector file (Shapefile for instance) path, - or a GeoJson dictionnary. These structures must contain a single Polygon or MultiPolygon. Multi-features are - not supported. - - Example of the "roi" parameter with a GeoJson dictionnary containing a Polygon as feature : - - .. code-block:: json - - { - "inputs": - { - "roi" : { - "type": "FeatureCollection", - "features": [ + Example of the "roi" parameter with a Shapefile + + .. code-block:: json + + { + "inputs": { - "type": "Feature", - "properties": {}, - "geometry": { - "coordinates": [ - [ - [5.194, 44.2064], - [5.194, 44.2059], - [5.195, 44.2059], - [5.195, 44.2064], - [5.194, 44.2064] - ] - ], - "type": "Polygon" - } + "roi" : "roi_vector_file.shp" } - ] - } - } - } + } - If the *debug_with_roi* advanced parameter (see dedicated tab) is enabled, the tiling of the entire image is kept but only the tiles intersecting - the ROI are computed. + .. tab:: Initial Elevation - MultiPolygon feature is only useful if the parameter *debug_with_roi* is activated, otherwise the total footprint of the - MultiPolygon will be used as ROI. + **Initial elevation** - By default epsg 4326 is used. If the user has defined a polygon in a different reference system, the "crs" field must be specified. + The attribute contains all informations about initial elevation: dem path, geoid and default altitude + + +-----------------------+----------------------------------------------------------------------------+--------+----------------------+----------------------+----------+ + | Name | Description | Type | Available value | Default value | Required | + +=======================+============================================================================+========+======================+======================+==========+ + | *dem* | Path to DEM tiles | string | | None | No | + +-----------------------+----------------------------------------------------------------------------+--------+----------------------+----------------------+----------+ + | *geoid* | Geoid path | string | | Cars internal geoid | No | + +-----------------------+----------------------------------------------------------------------------+--------+----------------------+----------------------+----------+ + | *altitude_delta_min* | constant delta in altitude (meters) between dem median and dem min | int | should be > 0 | None | No | + +-----------------------+----------------------------------------------------------------------------+--------+----------------------+----------------------+----------+ + | *altitude_delta_max* | constant delta in altitude (meters) between dem max and dem median | int | should be > 0 | None | No | + +-----------------------+----------------------------------------------------------------------------+--------+----------------------+----------------------+----------+ + + If no DEM path is provided, an internal dem is generated with sparse matches. If no geoid is provided, the default cars geoid is used (egm96). If no delta is provided, the dem_min and max generated with sparse matches will be used. + + The Deltas are used following this formula : - Example of the *debug_with_roi* mode utilizing an "roi" parameter of type MultiPolygon as a feature and a specific EPSG. + .. code-block:: python - .. code-block:: json + dem_min = initial_elevation - altitude_delta_min + dem_max = initial_elevation + altitude_delta_max + + .. warning:: Dem path is mandatory for the use of the altitude deltas. + + When there is no DEM data available, a default height above ellipsoid of 0 is used (no coverage for some points or pixels with no_data in the DEM tiles) + + Initial elevation can be provided as a dictionary with a field for each parameter, for example: + + + .. code-block:: json - { - "inputs": - { - "roi" : { - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "properties": {}, - "geometry": { - "coordinates": [ - [ - [ - [319700, 3317700], - [319800, 3317700], - [319800, 3317800], - [319800, 3317800], - [319700, 3317700] - ] - ], - [ - [ - [319900, 3317900], - [320000, 3317900], - [320000, 3318000], - [319900, 3318000], - [319900, 3317900] - ] - ] - ], - "type": "MultiPolygon" - } - } - ], - "crs" : { - "type": "name", - "properties": { - "name": "EPSG:32636" + "inputs": { + "initial_elevation": { + "dem": "/path/to/srtm.tif", + "geoid": "/path/to/geoid.tif", + "altitude_delta_min": 10, + "altitude_delta_max": 40 + } } } - }, - } - "advanced": - { - "debug_with_roi": true - } - } - Example of the "roi" parameter with a Shapefile + Alternatively, it can be set as a string corresponding to the DEM path, in which case default values for the geoid and the default altitude are used. - .. code-block:: json + .. code-block:: json - { - "inputs": - { - "roi" : "roi_vector_file.shp" - } - } + { + "inputs": { + "initial_elevation": "/path/to/srtm.tif" + } + } + Note that the geoid parameter in initial_elevation is not the geoid used for output products generated after the triangulation step + (see output parameters). + + Elevation management is tightly linked to the geometry plugin used. See :ref:`plugins` section for details - .. tab:: Orchestrator + .. tab:: Orchestrator CARS can distribute the computations chunks by using either dask (local or distributed cluster) or multiprocessing libraries. The distributed cluster require centralized files storage and uses PBS scheduler. @@ -550,877 +542,880 @@ The structure follows this organisation: In the case of distributed orchestration, the worker's logging output file is located in the workers_log directory (the message format indicates thread ID and process ID). A summary of basic profiling is generated in output directory. - .. tab:: Pipelines - - The ``pipeline`` key is optional and allows to choose the pipeline to use. By default CARS takes sensor images as inputs, and generates a DSM. - - The pipeline is a preconfigured application chain. For now, there are four pipelines. By default CARS launch a Sensor to Dense DSM pipeline. - - .. note:: - - The sensor_to_sparse_dsm pipeline can be used to prepare a refined configuration for the sensors_to_dense_dsm pipeline to facilitate and accelerate the sensors_to_dense_dsm pipeline. - See the `configuration/inputs/epipolar_a_priori` section for more details. + .. tab:: Pipeline configurations + The ``pipeline`` key is optional and allows users to choose the pipeline they would like to run. By default, CARS has a single pipeline: `default`. + This pipeline is modular and can be adapted to your needs. This sections provides examples of specific configurations. - This section describes the pipeline available in CARS. + Installed plugins may provide additional pipelines. The inputs and outputs are specific to each pipeline. This section describes the pipeline available in CARS. - +----------------+-----------------------+--------+------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+ - | Name | Description | Type | Default value | Available values | Required | - +================+=======================+========+====================================+======================================================================================================================================================================================================+==========+ - | *pipeline* | The pipeline to use | str | "sensors_to_dense_dsm_no_merging" | "sensors_to_dense_dsm", "sensors_to_sparse_dsm", "sensors_to_dense_depth_maps", "dense_depth_maps_to_dense_dsm", "sensors_to_dense_dsm_no_merging", "dense_depth_maps_to_dense_dsm_no_merging" | False | - +----------------+-----------------------+--------+------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------+ + +----------------+-----------------------+--------+---------------+------------------+----------+ + | Name | Description | Type | Default value | Available values | Required | + +================+=======================+========+===============+==================+==========+ + | *pipeline* | The pipeline to use | str | "default" | "default" | False | + +----------------+-----------------------+--------+---------------+------------------+----------+ + .. code-block:: json + { + "pipeline": "default" + } + .. tabs:: - .. code-block:: json - - { - "pipeline": "sensors_to_dense_dsm" - }, - - .. tabs:: - - .. tab:: Sensor to Dense DSM - - **Name**: "sensors_to_dense_dsm" - - **Description** - - .. figure:: ./images/cars_pipeline_sensor2dsm.png - :width: 700px - :align: center - - - For each stereo pair: - - 1. Create stereo-rectification grids for left and right views. - 2. Resample the both images into epipolar geometry. - 3. Compute sift matches between left and right views in epipolar geometry. - 4. Predict an optimal disparity range from the sift matches and create a bilinear correction model of the right image's stereo-rectification grid in order to minimize the epipolar error. Apply the estimated correction to the right grid. - 5. Resample again the stereo pair in epipolar geometry (using corrected grid for the right image) by using input :term:`dem` (such as SRTM) in order to reduce the disparity intervals to explore. - 6. Compute disparity for each image pair in epipolar geometry. - 7. Fill holes in disparity maps for each image pair in epipolar geometry. - 8. Triangule the matches and get for each pixel of the reference image a latitude, longitude and altitude coordinate. - - - Then + .. tab:: N inputs to 1 DSM - 9. Merge points clouds coming from each stereo pairs. - 10. Filter the resulting 3D points cloud via two consecutive filters: the first removes the small groups of 3D points, the second filters the points which have the most scattered neighbors. - 11. Rasterize: Project these altitudes on a regular grid as well as the associated color. + This is the default behavior of CARS. With inputs that are either sensor + image pairs or depth maps, CARS will automatically generate a single DSM. + The smallest configuration can simply contain only those inputs. - .. tab:: Sensor to Dense DSM no merging + .. note:: + The DSM will always be generated with all the inputs. - **Name**: "sensors_to_dense_dsm_no_merging" + When the ``merging`` parameter is set to `False`, the + combined point cloud containing all points + from the depth maps will be created on the fly + during the rasterization process. - **Description** + Conversely, if the ``merging`` parameter is set to `True`, + a point cloud will be generated from all depth maps + before the rasterization occurs. + This allows for point cloud filtering applications that can + consider all depth maps collectively. + + .. code-block:: json - .. figure:: ./images/cars_pipeline_sensor2dsm_no_merging.png - :width: 700px - :align: center + { - - For each stereo pair: + "inputs": { + + // sensor image pair(s) as inputs + "sensors" : { + "one": { + "image": "img1.tif", + "geomodel": "img1.geom" + }, + "two": { + "image": "img2.tif", + "geomodel": "img2.geom" + + }, + "three": { + "image": "img3.tif", + "geomodel": "img3.geom" + } + }, + "pairing": [["one", "two"],["one", "three"]] + + // or depth map(s) + "depth_maps": { + "my_name_for_this_depth_map": + { + "x" : "path_to_x.tif", + "y" : "path_to_y.tif", + "z" : "path_to_z.tif", + "color" : "path_to_color.tif", + "mask": "path_to_mask.tif", + "classification": "path_to_classification.tif", + "filling": "path_to_filling.tif", + "confidence": { + "confidence_name1": "path_to_confidence1.tif", + "confidence_name2": "path_to_confidence2.tif", + }, + "performance_map": "path_to_performance_map.tif", + "epsg": "depth_map_epsg" + } + } - 1. Create stereo-rectification grids for left and right views. - 2. Resample the both images into epipolar geometry. - 3. Compute sift matches between left and right views in epipolar geometry. - 4. Predict an optimal disparity range from the sift matches and create a bilinear correction model of the right image's stereo-rectification grid in order to minimize the epipolar error. Apply the estimated correction to the right grid. - 5. Resample again the stereo pair in epipolar geometry (using corrected grid for the right image) by using input :term:`dem` (such as SRTM) in order to reduce the disparity intervals to explore. - 6. Compute disparity for each image pair in epipolar geometry. - 7. Fill holes in disparity maps for each image pair in epipolar geometry. - 8. Triangule the matches and get for each pixel of the reference image a latitude, longitude and altitude coordinate. + } - - Then + } - 9. Rasterize: Project these altitudes on a regular grid as well as the associated color. - .. tab:: Sensor to Sparse DSM + .. tab:: Sparse DSMs - **Name**: "sensors_to_sparse_dsm" + In CARS, sparse DSMs are computed during the process of creating depth maps from sensor images (specifically during the `dem_generation` application). This means they cannot be created from depth maps. + It also means the program should be stopped even before finishing the first part of the pipeline (sensor images to depth maps) in order not to run useless applications. - **Description** + CARS provides an easy way of customizing the step at which the pipeline should be stopped. When the key ``product_level`` of ``output`` is empty, CARS will stop after the last application + whose ``save_intermediate_data`` key is set to True. - .. figure:: ./images/sensor_to_low_dsm.png - :width: 700px - :align: center + .. note:: + If the sparse DSMs have already been created, they can then be re-entered in CARS through the ``terrain_a_priori`` parameter, saving computation time. + Very useful when trying to test multiple configurations later in the pipeline ! - - For each stereo pair: + Applied to our current goal, this is the configuration needed to create sparse DSMs without useless applications running : - 1. Create stereo-rectification grids for left and right views. - 2. Resample the both images into epipolar geometry. - 3. Compute sift matches between left and right views in epipolar geometry. - 4. Predict an optimal disparity range from the sift matches and create a bilinear correction model of the right image's stereo-rectification grid in order to minimize the epipolar error. Apply the estimated correction to the right grid. - 5. Triangule the matches and get for each pixel of the reference image a latitude, longitude and altitude coordinate. + .. code-block:: json - - Then + { - 6. Merge points clouds coming from each stereo pairs. - 7. Filter the resulting 3D points cloud via two consecutive filters: the first removes the small groups of 3D points, the second filters the points which have the most scattered neighbors. - 8. Rasterize: Project these altitudes on a regular grid as well as the associated color. + "applications": { + "dem_generation": { + "save_intermediate_data": true + } + } + "output": { + "product_level": [] + } - .. tab:: Sensor to Dense Depth Maps + } - **Name**: "sensors_to_dense_depth_maps" + .. tab:: N pairs to N Depth Maps - **Description** + Depth maps are a way to represent point clouds as three images X Y and Z, each one representing the position of a pixel on its axis. + They are an official product of CARS, and can thus be created more easily than sparse DSMs. - .. figure:: ./images/cars_pipeline_sensor_to_pc.png - :width: 700px - :align: center + The ``product_level`` key in ``output`` can contain any combination of the values `dsm`, `depth_map`, and `point_cloud`. - - For each stereo pair: + Depth maps (one for each sensor pair) will be saved if `depth_map` is present in ``product_level`` : - 1. Create stereo-rectification grids for left and right views. - 2. Resample the both images into epipolar geometry. - 3. Compute sift matches between left and right views in epipolar geometry. - 4. Predict an optimal disparity range from the sift matches and create a bilinear correction model of the right image's stereo-rectification grid in order to minimize the epipolar error. Apply the estimated correction to the right grid. - 5. Resample again the stereo pair in epipolar geometry (using corrected grid for the right image) by using input :term:`dem` (such as SRTM) in order to reduce the disparity intervals to explore. - 6. Compute disparity for each image pair in epipolar geometry. - 7. Fill holes in disparity maps for each image pair in epipolar geometry. - 8. Triangule the matches and get for each pixel of the reference image a latitude, longitude and altitude coordinate. + .. code-block:: json + { - .. tab:: Dense Dense Depth Maps to Dense DSM + "output": { + "product_level": ["depth_map"] + } - **Name**: "dense_depth_maps_to_dense_dsm" + } - **Description** + .. tab:: N inputs to Point clouds + + Just like depth maps, the point cloud is an official product of CARS. As such, all that's needed is to add `point_cloud` to ``product_level`` in order for it to be generated. + + .. warning:: + CARS will only compute a point cloud when the key ``merging`` in ``advanced`` is set to `True`, which means + setting ``output_level`` as containing `point_cloud` will effectively force ``merging`` to `True`. + This behavior will have the side-effect of running the point cloud denoising and outliers removing applications. - .. figure:: ./images/pc_to_dsm.png - :width: 700px - :align: center + .. note:: + If you wish to save an individual point cloud for each input given, the key ``save_by_pair`` of ``output`` will need to be set to `True`. + .. code-block:: json - 1. Merge depth maps coming from each stereo pairs. - 2. Filter the resulting 3D points cloud via two consecutive filters: the first removes the small groups of 3D points, the second filters the points which have the most scattered neighbors. - 3. Rasterize: Project these altitudes on a regular grid as well as the associated color. + { - .. tab:: Dense Depth Maps to Dense DSM no merging + "output": { + "product_level": ["point_cloud"] + } - **Name**: "dense_depth_maps_to_dense_dsm_no_merging" + } - **Description** + .. tab:: Geometry plugin - .. figure:: ./images/pc_to_dsm_no_merging.png - :width: 700px - :align: center + This section describes configuration of the geometry plugins for CARS, please refer to :ref:`plugins` section for details on geometry plugins configuration. + +-------------------+-----------------------+--------+-------------------------+---------------------------------------+----------+ + | Name | Description | Type | Default value | Available values | Required | + +===================+=======================+========+=========================+=======================================+==========+ + | *geometry_plugin* | The plugin to use | str | "SharelocGeometry" | "SharelocGeometry" | False | + +-------------------+-----------------------+--------+-------------------------+---------------------------------------+----------+ - 1. Rasterize: Project these altitudes on a regular grid as well as the associated color. + .. code-block:: json + { + "geometry_plugin": "SharelocGeometry" + }, - .. tab:: Geometry plugin + .. tab:: Applications - This section describes configuration of the geometry plugins for CARS, please refer to :ref:`plugins` section for details on geometry plugins configuration. + This key is optional and allows to redefine parameters for each application used in pipeline. - +-------------------+-----------------------+--------+-------------------------+---------------------------------------+----------+ - | Name | Description | Type | Default value | Available values | Required | - +===================+=======================+========+=========================+=======================================+==========+ - | *geometry_plugin* | The plugin to use | str | "SharelocGeometry" | "SharelocGeometry" | False | - +-------------------+-----------------------+--------+-------------------------+---------------------------------------+----------+ + This section describes all possible configuration of CARS applications. - .. code-block:: json + CARS applications are defined and called by their name in applications configuration section: - { - "geometry_plugin": "SharelocGeometry" - }, + .. code-block:: json - .. tab:: Applications + "applications":{ + "application_name": { + "method": "application_dependent", + "parameter1": 3, + "parameter2": 0.3 + } + } - This key is optional and allows to redefine parameters for each application used in pipeline. - This section describes all possible configuration of CARS applications. + Be careful with these parameters: no mechanism ensures consistency between applications for now. + And some parameters can degrade performance and DSM quality heavily. + The default parameters have been set as a robust and consistent end to end configuration for the whole pipeline. - CARS applications are defined and called by their name in applications configuration section: + .. tabs:: - .. code-block:: json + .. tab:: Grid Generation - "applications":{ - "application_name": { - "method": "application_dependent", - "parameter1": 3, - "parameter2": 0.3 - } - }, + **Name**: "grid_generation" - Be careful with these parameters: no mechanism ensures consistency between applications for now. - And some parameters can degrade performance and DSM quality heavily. - The default parameters have been set as a robust and consistent end to end configuration for the whole pipeline. + **Description** - .. tabs:: + From sensors image, compute the stereo-rectification grids - .. tab:: Grid Generation + **Configuration** - **Name**: "grid_generation" + +-------------------------+-----------------------------------------------+---------+-----------------------------------+---------------+----------+ + | Name | Description | Type | Available values | Default value | Required | + +=========================+===============================================+=========+===================================+===============+==========+ + | method | Method for grid generation | string | "epipolar" | epipolar | No | + +-------------------------+-----------------------------------------------+---------+-----------------------------------+---------------+----------+ + | epi_step | Step of the deformation grid in nb. of pixels | int | should be > 0 | 30 | No | + +-------------------------+-----------------------------------------------+---------+-----------------------------------+---------------+----------+ + | save_intermediate_data | Save the generated grids | boolean | | false | No | + +-------------------------+-----------------------------------------------+---------+-----------------------------------+---------------+----------+ - **Description** + **Example** - From sensors image, compute the stereo-rectification grids + .. code-block:: json - **Configuration** + "applications": { + "grid_generation": { + "method": "epipolar", + "epi_step": 35 + } + }, - +-------------------------+-----------------------------------------------+---------+-----------------------------------+---------------+----------+ - | Name | Description | Type | Available values | Default value | Required | - +=========================+===============================================+=========+===================================+===============+==========+ - | method | Method for grid generation | string | "epipolar" | epipolar | No | - +-------------------------+-----------------------------------------------+---------+-----------------------------------+---------------+----------+ - | epi_step | Step of the deformation grid in nb. of pixels | int | should be > 0 | 30 | No | - +-------------------------+-----------------------------------------------+---------+-----------------------------------+---------------+----------+ - | save_intermediate_data | Save the generated grids | boolean | | false | No | - +-------------------------+-----------------------------------------------+---------+-----------------------------------+---------------+----------+ + .. tab:: Resampling - **Example** + **Name**: "resampling" - .. code-block:: json + **Description** - "applications": { - "grid_generation": { - "method": "epipolar", - "epi_step": 35 - } - }, + Input images are resampled with grids. - .. tab:: Resampling + **Configuration** - **Name**: "resampling" + +------------------------+--------------------------------------------------------+---------+-----------------+---------------+----------+ + | Name | Description | Type | Available value | Default value | Required | + +========================+========================================================+=========+=================+===============+==========+ + | method | Method for resampling | string | "bicubic" | "bicubic" | No | + +------------------------+--------------------------------------------------------+---------+-----------------+---------------+----------+ + | strip_height | Height of strip (only when tiling is done by strip) | int | should be > 0 | 60 | No | + +------------------------+--------------------------------------------------------+---------+-----------------+---------------+----------+ + | step | Horizontal step for resampling inside a strip | int | should be > 0 | 500 | No | + +------------------------+--------------------------------------------------------+---------+-----------------+---------------+----------+ + | save_intermediate_data | Save epipolar images and color | boolean | | false | No | + +------------------------+--------------------------------------------------------+---------+-----------------+---------------+----------+ - **Description** + **Example** - Input images are resampled with grids. + .. code-block:: json - **Configuration** + "applications": { + "resampling": { + "method": "bicubic", + "epi_tile_size": 600 + } + }, - +------------------------+--------------------------------------------------------+---------+-----------------+---------------+----------+ - | Name | Description | Type | Available value | Default value | Required | - +========================+========================================================+=========+=================+===============+==========+ - | method | Method for resampling | string | "bicubic" | "bicubic" | No | - +------------------------+--------------------------------------------------------+---------+-----------------+---------------+----------+ - | strip_height | Height of strip (only when tiling is done by strip) | int | should be > 0 | 60 | No | - +------------------------+--------------------------------------------------------+---------+-----------------+---------------+----------+ - | step | Horizontal step for resampling inside a strip | int | should be > 0 | 500 | No | - +------------------------+--------------------------------------------------------+---------+-----------------+---------------+----------+ - | save_intermediate_data | Save epipolar images and color | boolean | | false | No | - +------------------------+--------------------------------------------------------+---------+-----------------+---------------+----------+ + .. tab:: Sparse matching + + **Name**: "sparse_matching" + + **Description** + + Compute keypoints matches on pair images + + **Configuration** + + +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ + | Name | Description | Type | Available value | Default value | Required | + +======================================+================================================================================================+=============+========================+===============+==========+ + | method | Method for sparse matching | string | "sift" | "sift" | No | + +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ + | disparity_margin | Add a margin to min and max disparity as percent of the disparity range. | float | | 0.02 | No | + +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ + | elevation_delta_lower_bound | Expected lower bound for elevation delta with respect to input low resolution dem in meters | int, float | | -9000 | No | + +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ + | elevation_delta_upper_bound | Expected upper bound for elevation delta with respect to input low resolution dem in meters | int, float | | 9000 | No | + +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ + | epipolar_error_upper_bound | Expected upper bound for epipolar error in pixels | float | should be > 0 | 10.0 | No | + +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ + | epipolar_error_maximum_bias | Maximum bias for epipolar error in pixels | float | should be >= 0 | 0.0 | No | + +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ + | disparity_outliers_rejection_percent | Percentage of outliers to reject | float | between 0 and 1 | 0.1 | No | + +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ + | minimum_nb_matches | Minimum number of matches that must be computed to continue pipeline | int | should be > 0 | 100 | No | + +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ + | sift_matching_threshold | Threshold for the ratio to nearest second match | float | should be > 0 | 0.6 | No | + +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ + | sift_n_octave | The number of octaves of the Difference of Gaussians scale space | int | should be > 0 | 8 | No | + +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ + | sift_n_scale_per_octave | The numbers of levels per octave of the Difference of Gaussians scale space | int | should be > 0 | 3 | No | + +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ + | sift_peak_threshold | Constrast threshold to discard a match (at None it will be set according to image type) | float | should be > 0, or None | None | No | + +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ + | sift_edge_threshold | Distance to image edge threshold to discard a match | float | | -5.0 | No | + +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ + | sift_magnification | The descriptor magnification factor | float | should be > 0 | 2.0 | No | + +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ + | sift_back_matching | Also check that right vs. left gives same match | boolean | | true | No | + +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ + | matches_filter_knn | Number of neighbors used to measure isolation of matches and detect isolated matches | int | should be > 0 | 25 | No | + +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ + | matches_filter_dev_factor | Factor of deviation of isolation of matches to compute threshold of outliers | int, float | should be > 0 | 3.0 | No | + +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ + | save_intermediate_data | Save matches in epipolar geometry (4 first columns) and sensor geometry (4 last columns) | boolean | | false | No | + +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ + | strip_margin | Margin to use on strip | int | should be > 0 | 10 | No | + +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ + + + For more information about these parameters, please refer to the `VLFEAT SIFT documentation `_. + + .. note:: - **Example** + By default, the sift_peak_threshold is set to None (auto-mode). In this mode, the sift_peak_threshold is determined at runtime based on the sensor image type: - .. code-block:: json + * uint8 image type : sift_peak_threshold = 1 + * other image type sift_peak_threshold = 20 - "applications": { - "resampling": { - "method": "bicubic", - "epi_tile_size": 600 - } - }, + It is also possible to set the value to a fixed value. - .. tab:: Sparse matching - - **Name**: "sparse_matching" - - **Description** - - Compute keypoints matches on pair images - - **Configuration** - - +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ - | Name | Description | Type | Available value | Default value | Required | - +======================================+================================================================================================+=============+========================+===============+==========+ - | method | Method for sparse matching | string | "sift" | "sift" | No | - +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ - | disparity_margin | Add a margin to min and max disparity as percent of the disparity range. | float | | 0.02 | No | - +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ - | elevation_delta_lower_bound | Expected lower bound for elevation delta with respect to input low resolution dem in meters | int, float | | -9000 | No | - +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ - | elevation_delta_upper_bound | Expected upper bound for elevation delta with respect to input low resolution dem in meters | int, float | | 9000 | No | - +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ - | epipolar_error_upper_bound | Expected upper bound for epipolar error in pixels | float | should be > 0 | 10.0 | No | - +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ - | epipolar_error_maximum_bias | Maximum bias for epipolar error in pixels | float | should be >= 0 | 0.0 | No | - +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ - | disparity_outliers_rejection_percent | Percentage of outliers to reject | float | between 0 and 1 | 0.1 | No | - +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ - | minimum_nb_matches | Minimum number of matches that must be computed to continue pipeline | int | should be > 0 | 100 | No | - +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ - | sift_matching_threshold | Threshold for the ratio to nearest second match | float | should be > 0 | 0.6 | No | - +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ - | sift_n_octave | The number of octaves of the Difference of Gaussians scale space | int | should be > 0 | 8 | No | - +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ - | sift_n_scale_per_octave | The numbers of levels per octave of the Difference of Gaussians scale space | int | should be > 0 | 3 | No | - +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ - | sift_peak_threshold | Constrast threshold to discard a match (at None it will be set according to image type) | float | should be > 0, or None | None | No | - +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ - | sift_edge_threshold | Distance to image edge threshold to discard a match | float | | -5.0 | No | - +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ - | sift_magnification | The descriptor magnification factor | float | should be > 0 | 2.0 | No | - +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ - | sift_back_matching | Also check that right vs. left gives same match | boolean | | true | No | - +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ - | matches_filter_knn | Number of neighbors used to measure isolation of matches and detect isolated matches | int | should be > 0 | 25 | No | - +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ - | matches_filter_dev_factor | Factor of deviation of isolation of matches to compute threshold of outliers | int, float | should be > 0 | 3.0 | No | - +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ - | save_intermediate_data | Save matches in epipolar geometry (4 first columns) and sensor geometry (4 last columns) | boolean | | false | No | - +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ - | strip_margin | Margin to use on strip | int | should be > 0 | 10 | No | - +--------------------------------------+------------------------------------------------------------------------------------------------+-------------+------------------------+---------------+----------+ - - - For more information about these parameters, please refer to the `VLFEAT SIFT documentation `_. - - .. note:: + **Example** - By default, the sift_peak_threshold is set to None (auto-mode). In this mode, the sift_peak_threshold is determined at runtime based on the sensor image type: + .. code-block:: json - * uint8 image type : sift_peak_threshold = 1 - * other image type sift_peak_threshold = 20 + "applications": { + "sparse_matching": { + "method": "sift", + "disparity_margin": 0.01 + } + }, - It is also possible to set the value to a fixed value. + .. tab:: dem Generation - **Example** + **Name**: "dem_generation" - .. code-block:: json + **Description** - "applications": { - "sparse_matching": { - "method": "sift", - "disparity_margin": 0.01 - } - }, + Generates dem from sparse matches. - .. tab:: dem Generation + 3 dems are generated, with different methods: + * median + * min + * max - **Name**: "dem_generation" + The DEMs are generated in the application dump directory - **Description** + **Configuration** - Generates dem from sparse matches. + +---------------------------------+------------------------------------------------------------+------------+-----------------+---------------+----------+ + | Name | Description | Type | Available value | Default value | Required | + +=================================+============================================================+============+=================+===============+==========+ + | method | Method for dem_generation | string | "dichotomic" | "dichotomic" | Yes | + +---------------------------------+------------------------------------------------------------+------------+-----------------+---------------+----------+ + | resolution | Resolution of dem, in meter | int, float | should be > 0 | 200 | No | + +---------------------------------+------------------------------------------------------------+------------+-----------------+---------------+----------+ + | margin | Margin to use on the border of dem, in meter | int, float | should be > 0 | 6000 | No | + +---------------------------------+------------------------------------------------------------+------------+-----------------+---------------+----------+ + | percentile | Percentile of matches to ignore in min and max functions | int | should be > 0 | 3 | No | + +---------------------------------+------------------------------------------------------------+------------+-----------------+---------------+----------+ + | min_number_matches | Minimum number of matches needed to have a valid tile | int | should be > 0 | 30 | No | + +---------------------------------+------------------------------------------------------------+------------+-----------------+---------------+----------+ + | height_margin | Height margin [margin min, margin max], in meter | int | | 20 | No | + +---------------------------------+------------------------------------------------------------+------------+-----------------+---------------+----------+ + | fillnodata_max_search_distance | Max search distance for rasterio fill nodata | int | should be > 0 | 3 | No | + +---------------------------------+------------------------------------------------------------+------------+-----------------+---------------+----------+ - 3 dems are generated, with different methods: - * median - * min - * max + **Example** - The DEMs are generated in the application dump directory + .. code-block:: json - **Configuration** + "applications": { + "dem_generation": { + "method": "dichotomic", + "min_number_matches": 20 + } - +---------------------------------+------------------------------------------------------------+------------+-----------------+---------------+----------+ - | Name | Description | Type | Available value | Default value | Required | - +=================================+============================================================+============+=================+===============+==========+ - | method | Method for dem_generation | string | "dichotomic" | "dichotomic" | Yes | - +---------------------------------+------------------------------------------------------------+------------+-----------------+---------------+----------+ - | resolution | Resolution of dem, in meter | int, float | should be > 0 | 200 | No | - +---------------------------------+------------------------------------------------------------+------------+-----------------+---------------+----------+ - | margin | Margin to use on the border of dem, in meter | int, float | should be > 0 | 6000 | No | - +---------------------------------+------------------------------------------------------------+------------+-----------------+---------------+----------+ - | percentile | Percentile of matches to ignore in min and max functions | int | should be > 0 | 3 | No | - +---------------------------------+------------------------------------------------------------+------------+-----------------+---------------+----------+ - | min_number_matches | Minimum number of matches needed to have a valid tile | int | should be > 0 | 30 | No | - +---------------------------------+------------------------------------------------------------+------------+-----------------+---------------+----------+ - | height_margin | Height margin [margin min, margin max], in meter | int | | 20 | No | - +---------------------------------+------------------------------------------------------------+------------+-----------------+---------------+----------+ - | fillnodata_max_search_distance | Max search distance for rasterio fill nodata | int | should be > 0 | 3 | No | - +---------------------------------+------------------------------------------------------------+------------+-----------------+---------------+----------+ + .. tab:: Dense matching - **Example** + **Name**: "dense_matching" - .. code-block:: json + **Description** - "applications": { - "dem_generation": { - "method": "dichotomic", - "min_number_matches": 20 - } + Compute the disparity map from stereo-rectified pair images + + .. list-table:: Configuration + :widths: 19 19 19 19 19 19 + :header-rows: 1 + + * - Name + - Description + - Type + - Available value + - Default value + - Required + * - method + - Method for dense matching + - string + - "census_sgm", "mccnn_sgm" + - "census_sgm" + - No + * - loader + - external library use to compute dense matching + - string + - "pandora" + - "pandora" + - No + * - loader_conf + - Configuration associated with loader, dictionary or path to config + - dict or str + - + - + - No + * - min_elevation_offset + - Override minimum disparity from prepare step with this offset in meters + - int + - + - None + - No + * - max_elevation_offset + - Override maximum disparity from prepare step with this offset in meters + - int + - should be > min + - None + - No + * - disp_min_threshold + - Override minimum disparity when less than lower bound + - int + - + - None + - No + * - disp_max_threshold + - Override maximum disparity when greater than upper bound + - int + - should be > min + - None + - No + * - min_epi_tile_size + - Lower bound of optimal epipolar tile size for dense matching + - int + - should be > 0 + - 300 + - No + * - max_epi_tile_size + - Upper bound of optimal epipolar tile size for dense matching + - int + - should be > 0 and > min + - 1500 + - No + * - epipolar_tile_margin_in_percent + - Size of the margin used for dense matching (percent of tile size) + - int + - + - 60 + - No + * - generate_performance_map + - Generate a performance map from disparity map + - boolean + - + - False + - No + * - generate_confidence_intervals + - Compute confidence intervals from disparity map. + - boolean + - + - False + - No + * - perf_eta_max_ambiguity + - Ambiguity confidence eta max used for performance map + - float + - + - 0.99 + - No + * - perf_eta_max_risk + - Risk confidence eta max used for performance map + - float + - + - 0.25 + - No + * - perf_eta_step + - Risk and Ambiguity confidence eta step used for performance map + - float + - + - 0.04 + - No + * - perf_ambiguity_threshold + - Maximal ambiguity considered for performance map + - float + - + - 0.6 + - No + * - save_intermediate_data + - Save disparity map and disparity confidence + - boolean + - + - false + - No + * - use_global_disp_range + - If true, use global disparity range, otherwise local range estimation + - boolean + - + - false + - No + * - local_disp_grid_step + - Step of disparity min/ max grid used to resample dense disparity range + - int + - + - 30 + - No + * - disp_range_propagation_filter_size + - Filter size of local min/max disparity, to propagate local min/max + - int + - should be > 0 + - 300 + - No + * - use_cross_validation + - Add cross validation step + - bool + - + - false + - No + + See `Pandora documentation `_ for more information. + + **Example** + + .. code-block:: json - .. tab:: Dense matching + "applications": { + "dense_matching": { + "method": "census_sgm", + "loader": "pandora", + "loader_conf": "path_to_user_pandora_configuration" + } + }, - **Name**: "dense_matching" + .. note:: - **Description** + * Disparity range can be global (same disparity range used for each tile), or local (disparity range is estimated for each tile with dem min/max). + * When user activate the generation of performance map, this map transits until being rasterized. Performance map is managed as a confidence map. + * To save the confidence, the save_intermediate_data parameter should be activated. - Compute disparity map from stereo-rectified pair images - .. list-table:: Configuration - :widths: 19 19 19 19 19 19 - :header-rows: 1 - - * - Name - - Description - - Type - - Available value - - Default value - - Required - * - method - - Method for dense matching - - string - - "census_sgm", "mccnn_sgm" - - "census_sgm" - - No - * - loader - - external library use to compute dense matching - - string - - "pandora" - - "pandora" - - No - * - loader_conf - - Configuration associated with loader, dictionary or path to config - - dict or str - - - - - - No - * - min_elevation_offset - - Override minimum disparity from prepare step with this offset in meters - - int - - - - None - - No - * - max_elevation_offset - - Override maximum disparity from prepare step with this offset in meters - - int - - should be > min - - None - - No - * - disp_min_threshold - - Override minimum disparity when less than lower bound - - int - - - - None - - No - * - disp_max_threshold - - Override maximum disparity when greater than upper bound - - int - - should be > min - - None - - No - * - min_epi_tile_size - - Lower bound of optimal epipolar tile size for dense matching - - int - - should be > 0 - - 300 - - No - * - max_epi_tile_size - - Upper bound of optimal epipolar tile size for dense matching - - int - - should be > 0 and > min - - 1500 - - No - * - epipolar_tile_margin_in_percent - - Size of the margin used for dense matching (percent of tile size) - - int - - - - 60 - - No - * - generate_performance_map - - Generate a performance map from disparity map - - boolean - - - - False - - No - * - generate_confidence_intervals - - Compute confidence intervals from disparity map. - - boolean - - - - False - - No - * - perf_eta_max_ambiguity - - Ambiguity confidence eta max used for performance map - - float - - - - 0.99 - - No - * - perf_eta_max_risk - - Risk confidence eta max used for performance map - - float - - - - 0.25 - - No - * - perf_eta_step - - Risk and Ambiguity confidence eta step used for performance map - - float - - - - 0.04 - - No - * - perf_ambiguity_threshold - - Maximal ambiguity considered for performance map - - float - - - - 0.6 - - No - * - save_intermediate_data - - Save disparity map and disparity confidence - - boolean - - - - false - - No - * - use_global_disp_range - - If true, use global disparity range, otherwise local range estimation - - boolean - - - - false - - No - * - local_disp_grid_step - - Step of disparity min/ max grid used to resample dense disparity range - - int - - - - 30 - - No - * - disp_range_propagation_filter_size - - Filter size of local min/max disparity, to propagate local min/max - - int - - should be > 0 - - 300 - - No - * - use_cross_validation - - Add cross validation step - - bool - - - - false - - No - - See `Pandora documentation `_ for more information. - - **Example** - - .. code-block:: json - - "applications": { - "dense_matching": { - "method": "census_sgm", - "loader": "pandora", - "loader_conf": "path_to_user_pandora_configuration" - } - }, - - .. note:: - - * Disparity range can be global (same disparity range used for each tile), or local (disparity range is estimated for each tile with dem min/max). - * When user activate the generation of performance map, this map transits until being rasterized. Performance map is managed as a confidence map. - * To save the confidence in the sensors_to_dense_point_clouds pipeline, the save_intermediate_data parameter should be activated. - - - .. tab:: Dense matches filling + .. tab:: Dense matches filling - **Name**: "dense_matches_filling" + **Name**: "dense_matches_filling" - **Description** + **Description** - Fill holes in dense matches map. This uses the holes detected with the HolesDetection application. - The holes correspond to the area masked for dense matching. + Fill holes in dense matches map. This uses the holes detected with the HolesDetection application. + The holes correspond to the area masked for dense matching. - **Configuration** + **Configuration** - +-------------------------------------+---------------------------------+---------+-------------------------+--------------------+----------+ - | Name | Description | Type | Available value | Default value | Required | - +=====================================+=================================+=========+=========================+====================+==========+ - | method | Method for holes detection | string | "plane", "zero_padding" | "plane" | No | - +-------------------------------------+---------------------------------+---------+-------------------------+--------------------+----------+ - | save_intermediate_data | Save disparity map | boolean | | False | No | - +-------------------------------------+---------------------------------+---------+-------------------------+--------------------+----------+ + +-------------------------------------+---------------------------------+---------+-------------------------+--------------------+----------+ + | Name | Description | Type | Available value | Default value | Required | + +=====================================+=================================+=========+=========================+====================+==========+ + | method | Method for holes detection | string | "plane", "zero_padding" | "plane" | No | + +-------------------------------------+---------------------------------+---------+-------------------------+--------------------+----------+ + | save_intermediate_data | Save disparity map | boolean | | False | No | + +-------------------------------------+---------------------------------+---------+-------------------------+--------------------+----------+ - **Method plane:** + **Method plane:** - +-------------------------------------+---------------------------------+-------------+-------------------------+--------------------+----------+ - | Name | Description | Type | Available value | Default value | Required | - +=====================================+=================================+=============+=========================+====================+==========+ - | classification | Classification band name | List[str] | | None | No | - +-------------------------------------+---------------------------------+-------------+-------------------------+--------------------+----------+ - | interpolation_type | Interpolation type | string | "pandora" | "pandora" | No | - +-------------------------------------+---------------------------------+-------------+-------------------------+--------------------+----------+ - | interpolation_method | Method for holes interpolation | string | "mc_cnn" | "mc_cnn" | No | - +-------------------------------------+---------------------------------+-------------+-------------------------+--------------------+----------+ - | max_search_distance | Maximum search distance | int | | 100 | No | - +-------------------------------------+---------------------------------+-------------+-------------------------+--------------------+----------+ - | smoothing_iterations | Number of smoothing iterations | int | | 1 | No | - +-------------------------------------+---------------------------------+-------------+-------------------------+--------------------+----------+ - | ignore_nodata_at_disp_mask_borders | Ignore nodata at borders | boolean | | false | No | - +-------------------------------------+---------------------------------+-------------+-------------------------+--------------------+----------+ - | ignore_zero_fill_disp_mask_values | Ignore zeros | boolean | | true | No | - +-------------------------------------+---------------------------------+-------------+-------------------------+--------------------+----------+ - | ignore_extrema_disp_values | Ignore extrema values | boolean | | true | No | - +-------------------------------------+---------------------------------+-------------+-------------------------+--------------------+----------+ - | nb_pix | Margin used for mask | int | | 20 | No | - +-------------------------------------+---------------------------------+-------------+-------------------------+--------------------+----------+ - | percent_to_erode | Percentage to erode | float | | 0.2 | No | - +-------------------------------------+---------------------------------+-------------+-------------------------+--------------------+----------+ + +-------------------------------------+---------------------------------+-------------+-------------------------+--------------------+----------+ + | Name | Description | Type | Available value | Default value | Required | + +=====================================+=================================+=============+=========================+====================+==========+ + | classification | Classification band name | List[str] | | None | No | + +-------------------------------------+---------------------------------+-------------+-------------------------+--------------------+----------+ + | interpolation_type | Interpolation type | string | "pandora" | "pandora" | No | + +-------------------------------------+---------------------------------+-------------+-------------------------+--------------------+----------+ + | interpolation_method | Method for holes interpolation | string | "mc_cnn" | "mc_cnn" | No | + +-------------------------------------+---------------------------------+-------------+-------------------------+--------------------+----------+ + | max_search_distance | Maximum search distance | int | | 100 | No | + +-------------------------------------+---------------------------------+-------------+-------------------------+--------------------+----------+ + | smoothing_iterations | Number of smoothing iterations | int | | 1 | No | + +-------------------------------------+---------------------------------+-------------+-------------------------+--------------------+----------+ + | ignore_nodata_at_disp_mask_borders | Ignore nodata at borders | boolean | | false | No | + +-------------------------------------+---------------------------------+-------------+-------------------------+--------------------+----------+ + | ignore_zero_fill_disp_mask_values | Ignore zeros | boolean | | true | No | + +-------------------------------------+---------------------------------+-------------+-------------------------+--------------------+----------+ + | ignore_extrema_disp_values | Ignore extrema values | boolean | | true | No | + +-------------------------------------+---------------------------------+-------------+-------------------------+--------------------+----------+ + | nb_pix | Margin used for mask | int | | 20 | No | + +-------------------------------------+---------------------------------+-------------+-------------------------+--------------------+----------+ + | percent_to_erode | Percentage to erode | float | | 0.2 | No | + +-------------------------------------+---------------------------------+-------------+-------------------------+--------------------+----------+ - **Method zero_padding:** + **Method zero_padding:** - The zero_padding method fills the disparity with zeros where the selected classification values are non-zero values. + The zero_padding method fills the disparity with zeros where the selected classification values are non-zero values. - +-------------------------------------+---------------------------------+-----------+-------------------------+--------------------+----------+ - | Name | Description | Type | Available value | Default value | Required | - +=====================================+=================================+===========+=========================+====================+==========+ - | classification | Classification band name | List[str] | | None | No | - +-------------------------------------+---------------------------------+-----------+-------------------------+--------------------+----------+ + +-------------------------------------+---------------------------------+-----------+-------------------------+--------------------+----------+ + | Name | Description | Type | Available value | Default value | Required | + +=====================================+=================================+===========+=========================+====================+==========+ + | classification | Classification band name | List[str] | | None | No | + +-------------------------------------+---------------------------------+-----------+-------------------------+--------------------+----------+ - .. note:: - - The classification of second input is not given. Only the first disparity will be filled with zero value. - - The filled area will be considered as a valid disparity mask. + .. note:: + - The classification of second input is not given. Only the first disparity will be filled with zero value. + - The filled area will be considered as a valid disparity mask. - .. warning:: + .. warning:: - There is a particular case with the *dense_matches_filling* application because it is called twice. - The eighth step consists of fill dense matches via two consecutive methods. - So you can configure the application twice , once for the *plane*, the other for *zero_padding* method. - Because it is not possible to define twice the *application_name* on your json configuration file, we have decided to configure - those two applications with : + There is a particular case with the *dense_matches_filling* application because it is called twice. + The eighth step consists of fill dense matches via two consecutive methods. + So you can configure the application twice , once for the *plane*, the other for *zero_padding* method. + Because it is not possible to define twice the *application_name* on your json configuration file, we have decided to configure + those two applications with : - * *dense_matches_filling.1* - * *dense_matches_filling.2* + * *dense_matches_filling.1* + * *dense_matches_filling.2* - Each one is associated to a particular *dense_matches_filling* method* + Each one is associated to a particular *dense_matches_filling* method* - **Example** + **Example** - .. code-block:: json + .. code-block:: json - "applications": { - "dense_matches_filling.1": { - "method": "plane", - "classification": ["water"], - "save_intermediate_data": true + "applications": { + "dense_matches_filling.1": { + "method": "plane", + "classification": ["water"], + "save_intermediate_data": true + }, + "dense_matches_filling.2": { + "method": "zero_padding", + "classification": ["cloud", "snow"], + "save_intermediate_data": true + } }, - "dense_matches_filling.2": { - "method": "zero_padding", - "classification": ["cloud", "snow"], - "save_intermediate_data": true - } - }, - - - .. tab:: Triangulation - **Name**: "triangulation" - **Description** + .. tab:: Triangulation - Triangulating the sights and get for each point of the reference image a latitude, longitude, altitude point + **Name**: "triangulation" - **Configuration** + **Description** - +------------------------+--------------------------------------------------------------------------------------------------------------------+---------+--------------------------------------+------------------------------+----------+ - | Name | Description | Type | Available values | Default value | Required | - +========================+====================================================================================================================+=========+======================================+==============================+==========+ - | method | Method for triangulation | string | "line_of_sight_intersection" | "line_of_sight_intersection" | No | - +------------------------+--------------------------------------------------------------------------------------------------------------------+---------+--------------------------------------+------------------------------+----------+ - | snap_to_img1 | If all pairs share the same left image, modify lines of sights of secondary images to cross those of the ref image | boolean | | false | No | - +------------------------+--------------------------------------------------------------------------------------------------------------------+---------+--------------------------------------+------------------------------+----------+ - | save_intermediate_data | Save depth map | boolean | | false | No | - +------------------------+--------------------------------------------------------------------------------------------------------------------+---------+--------------------------------------+------------------------------+----------+ - - **Example** - - .. code-block:: json - - "applications": { - "triangulation": { - "method": "line_of_sight_intersection", - "snap_to_img1": true - } - }, + Triangulating the sights and get for each point of the reference image a latitude, longitude, altitude point - .. tab:: Point Cloud fusion + **Configuration** - **Name**: "point_cloud_fusion" + +------------------------+--------------------------------------------------------------------------------------------------------------------+---------+--------------------------------------+------------------------------+----------+ + | Name | Description | Type | Available values | Default value | Required | + +========================+====================================================================================================================+=========+======================================+==============================+==========+ + | method | Method for triangulation | string | "line_of_sight_intersection" | "line_of_sight_intersection" | No | + +------------------------+--------------------------------------------------------------------------------------------------------------------+---------+--------------------------------------+------------------------------+----------+ + | snap_to_img1 | If all pairs share the same left image, modify lines of sights of secondary images to cross those of the ref image | boolean | | false | No | + +------------------------+--------------------------------------------------------------------------------------------------------------------+---------+--------------------------------------+------------------------------+----------+ + | save_intermediate_data | Save depth map | boolean | | false | No | + +------------------------+--------------------------------------------------------------------------------------------------------------------+---------+--------------------------------------+------------------------------+----------+ - **Description** + **Example** - Merge points clouds coming from each pair - - Only one method is available for now: "mapping_to_terrain_tiles" - - **Configuration** - - +------------------------------+------------------------------------------+---------+----------------------------+----------------------------+----------+ - | Name | Description | Type | Available value | Default value | Required | - +==============================+==========================================+=========+============================+============================+==========+ - | method | Method for fusion | string | "mapping_to_terrain_tiles" | "mapping_to_terrain_tiles" | No | - +------------------------------+------------------------------------------+---------+----------------------------+----------------------------+----------+ - | save_intermediate_data | Save points clouds as laz and csv format | boolean | | false | No | - +------------------------------+------------------------------------------+---------+----------------------------+----------------------------+----------+ - | save_by_pair | Enable points cloud saving by pair | boolean | | false | No | - +------------------------------+------------------------------------------+---------+----------------------------+----------------------------+----------+ - - **Example** - - - .. code-block:: json + .. code-block:: json "applications": { - "point_cloud_fusion": { - "method": "mapping_to_terrain_tiles", - "save_intermediate_data": true, - "save_by_pair": true, + "triangulation": { + "method": "line_of_sight_intersection", + "snap_to_img1": true } }, - .. note:: - When `save_intermediate_data` is activated, multiple Laz and csv files are saved, corresponding to each processed terrain tiles. - Please, see the section :ref:`merge_laz_files` to merge them into one single file. - `save_by_pair` parameter enables saving by input pair. The csv/laz name aggregates row, col and corresponding pair key. - - .. tab:: Point Cloud outliers removing - - **Name**: "point_cloud_outliers_removing" - - **Description** - - Point cloud outliers removing - - **Configuration** + .. tab:: Point Cloud fusion - +------------------------------+------------------------------------------+---------+-----------------------------------+---------------+----------+ - | Name | Description | Type | Available value | Default value | Required | - +==============================+==========================================+=========+===================================+===============+==========+ - | method | Method for point cloud outliers removing | string | "statistical", "small_components" | "statistical" | No | - +------------------------------+------------------------------------------+---------+-----------------------------------+---------------+----------+ - | save_intermediate_data | Save points clouds as laz and csv format | boolean | | false | No | - +------------------------------+------------------------------------------+---------+-----------------------------------+---------------+----------+ - | save_by_pair | Enable points cloud saving by pair | boolean | | false | No | - +------------------------------+------------------------------------------+---------+-----------------------------------+---------------+----------+ + **Name**: "point_cloud_fusion" - If method is *statistical*: + **Description** - +----------------+-------------+---------+-----------------+---------------+----------+ - | Name | Description | Type | Available value | Default value | Required | - +================+=============+=========+=================+===============+==========+ - | activated | | boolean | | false | No | - +----------------+-------------+---------+-----------------+---------------+----------+ - | k | | int | should be > 0 | 50 | No | - +----------------+-------------+---------+-----------------+---------------+----------+ - | std_dev_factor | | float | should be > 0 | 5.0 | No | - +----------------+-------------+---------+-----------------+---------------+----------+ + Merge points clouds coming from each pair - If method is *small_components* + Only one method is available for now: "mapping_to_terrain_tiles" - +-----------------------------+-------------+---------+-----------------+---------------+----------+ - | Name | Description | Type | Available value | Default value | Required | - +=============================+=============+=========+=================+===============+==========+ - | activated | | boolean | | false | No | - +-----------------------------+-------------+---------+-----------------+---------------+----------+ - | on_ground_margin | | int | | 10 | No | - +-----------------------------+-------------+---------+-----------------+---------------+----------+ - | connection_distance | | float | | 3.0 | No | - +-----------------------------+-------------+---------+-----------------+---------------+----------+ - | nb_points_threshold | | int | | 50 | No | - +-----------------------------+-------------+---------+-----------------+---------------+----------+ - | clusters_distance_threshold | | float | | None | No | - +-----------------------------+-------------+---------+-----------------+---------------+----------+ + **Configuration** - .. warning:: + +------------------------------+------------------------------------------+---------+----------------------------+----------------------------+----------+ + | Name | Description | Type | Available value | Default value | Required | + +==============================+==========================================+=========+============================+============================+==========+ + | method | Method for fusion | string | "mapping_to_terrain_tiles" | "mapping_to_terrain_tiles" | No | + +------------------------------+------------------------------------------+---------+----------------------------+----------------------------+----------+ + | save_intermediate_data | Save points clouds as laz and csv format | boolean | | false | No | + +------------------------------+------------------------------------------+---------+----------------------------+----------------------------+----------+ + | save_by_pair | Enable points cloud saving by pair | boolean | | false | No | + +------------------------------+------------------------------------------+---------+----------------------------+----------------------------+----------+ - There is a particular case with the *Point Cloud outliers removing* application because it is called twice. - The ninth step consists of Filter the 3D points cloud via two consecutive filters. - So you can configure the application twice , once for the *small component filters*, the other for *statistical* filter. - Because it is not possible to define twice the *application_name* on your json configuration file, we have decided to configure - those two applications with : + **Example** - * *point_cloud_outliers_removing.1* - * *point_cloud_outliers_removing.2* - Each one is associated to a particular *point_cloud_outliers_removing* method* + .. code-block:: json - **Example** - - .. code-block:: json + "applications": { + "point_cloud_fusion": { + "method": "mapping_to_terrain_tiles", + "save_intermediate_data": true, + "save_by_pair": true, + } + }, - "applications": { - "point_cloud_outliers_removing.1": { - "method": "small_components", - "on_ground_margin": 10, - "save_intermediate_data": true + .. note:: + When `save_intermediate_data` is activated, multiple Laz and csv files are saved, corresponding to each processed terrain tiles. + Please, see the section :ref:`merge_laz_files` to merge them into one single file. + `save_by_pair` parameter enables saving by input pair. The csv/laz name aggregates row, col and corresponding pair key. + + .. tab:: Point Cloud outliers removing + + **Name**: "point_cloud_outliers_removing" + + **Description** + + Point cloud outliers removing + + **Configuration** + + +------------------------------+------------------------------------------+---------+-----------------------------------+---------------+----------+ + | Name | Description | Type | Available value | Default value | Required | + +==============================+==========================================+=========+===================================+===============+==========+ + | method | Method for point cloud outliers removing | string | "statistical", "small_components" | "statistical" | No | + +------------------------------+------------------------------------------+---------+-----------------------------------+---------------+----------+ + | save_intermediate_data | Save points clouds as laz and csv format | boolean | | false | No | + +------------------------------+------------------------------------------+---------+-----------------------------------+---------------+----------+ + | save_by_pair | Enable points cloud saving by pair | boolean | | false | No | + +------------------------------+------------------------------------------+---------+-----------------------------------+---------------+----------+ + + If method is *statistical*: + + +----------------+-------------+---------+-----------------+---------------+----------+ + | Name | Description | Type | Available value | Default value | Required | + +================+=============+=========+=================+===============+==========+ + | activated | | boolean | | false | No | + +----------------+-------------+---------+-----------------+---------------+----------+ + | k | | int | should be > 0 | 50 | No | + +----------------+-------------+---------+-----------------+---------------+----------+ + | std_dev_factor | | float | should be > 0 | 5.0 | No | + +----------------+-------------+---------+-----------------+---------------+----------+ + + If method is *small_components* + + +-----------------------------+-------------+---------+-----------------+---------------+----------+ + | Name | Description | Type | Available value | Default value | Required | + +=============================+=============+=========+=================+===============+==========+ + | activated | | boolean | | false | No | + +-----------------------------+-------------+---------+-----------------+---------------+----------+ + | on_ground_margin | | int | | 10 | No | + +-----------------------------+-------------+---------+-----------------+---------------+----------+ + | connection_distance | | float | | 3.0 | No | + +-----------------------------+-------------+---------+-----------------+---------------+----------+ + | nb_points_threshold | | int | | 50 | No | + +-----------------------------+-------------+---------+-----------------+---------------+----------+ + | clusters_distance_threshold | | float | | None | No | + +-----------------------------+-------------+---------+-----------------+---------------+----------+ + + .. warning:: + + There is a particular case with the *Point Cloud outliers removing* application because it is called twice. + The ninth step consists of Filter the 3D points cloud via two consecutive filters. + So you can configure the application twice , once for the *small component filters*, the other for *statistical* filter. + Because it is not possible to define twice the *application_name* on your json configuration file, we have decided to configure + those two applications with : + + * *point_cloud_outliers_removing.1* + * *point_cloud_outliers_removing.2* + + Each one is associated to a particular *point_cloud_outliers_removing* method* + + **Example** + + .. code-block:: json + + "applications": { + "point_cloud_outliers_removing.1": { + "method": "small_components", + "on_ground_margin": 10, + "save_intermediate_data": true + }, + "point_cloud_outliers_removing.2": { + "method": "statistical", + "k": 10, + "save_intermediate_data": true, + "save_by_pair": true, + } }, - "point_cloud_outliers_removing.2": { - "method": "statistical", - "k": 10, - "save_intermediate_data": true, - "save_by_pair": true, - } - }, - .. tab:: Point Cloud Rasterization + .. tab:: Point Cloud Rasterization - **Name**: "point_cloud_rasterization" + **Name**: "point_cloud_rasterization" - **Description** + **Description** - Project altitudes on regular grid. + Project altitudes on regular grid. - Only one simple gaussian method is available for now. - - .. list-table:: Configuration - :widths: 19 19 19 19 19 19 - :header-rows: 1 - - * - Name - - Description - - Type - - Available value - - Default value - - Required - * - method - - - - string - - "simple_gaussian" - - simple_gaussian - - No - * - dsm_radius - - - - float, int - - - - 1.0 - - No - * - sigma - - - - float - - - - None - - No - * - grid_points_division_factor - - - - int - - - - None - - No - * - dsm_no_data - - - - int - - - - -32768 - - - * - color_no_data - - - - int - - - - 0 - - - * - color_dtype - - | By default, it's retrieved from the input color - | Otherwise, specify an image type - - string - - | "uint8", "uint16" - | "float32" ... - - None - - No - * - msk_no_data - - No data value for mask and classif - - int - - - - 255 - - - * - save_intermediate_data - - Save all layers from input point cloud in application `dump_dir` - - boolean - - - - false - - No - - **Example** - - .. code-block:: json + Only one simple gaussian method is available for now. + + .. list-table:: Configuration + :widths: 19 19 19 19 19 19 + :header-rows: 1 + + * - Name + - Description + - Type + - Available value + - Default value + - Required + * - method + - + - string + - "simple_gaussian" + - simple_gaussian + - No + * - dsm_radius + - + - float, int + - + - 1.0 + - No + * - sigma + - + - float + - + - None + - No + * - grid_points_division_factor + - + - int + - + - None + - No + * - dsm_no_data + - + - int + - + - -32768 + - + * - color_no_data + - + - int + - + - 0 + - + * - color_dtype + - | By default, it's retrieved from the input color + | Otherwise, specify an image type + - string + - | "uint8", "uint16" + | "float32" ... + - None + - No + * - msk_no_data + - No data value for mask and classif + - int + - + - 255 + - + * - save_intermediate_data + - Save all layers from input point cloud in application `dump_dir` + - boolean + - + - false + - No + + **Example** + + .. code-block:: json "applications": { "point_cloud_rasterization": { @@ -1429,7 +1424,7 @@ The structure follows this organisation: } }, - .. tab:: Advanced parameters + .. tab:: Advanced parameters +----------------------------+-------------------------------------------------------------------------+-----------------------+----------------------+----------+ @@ -1460,13 +1455,10 @@ The structure follows this organisation: **Epipolar a priori** - The epipolar a priori is useful to accelerate the preliminary steps of the grid correction and the disparity range evaluation, - particularly for the sensor_to_full_resolution_dsm pipeline. - The epipolar_a_priori data dict is produced during low or full resolution dsm pipeline. - However, the epipolar_a_priori should be not activated for the sensor_to_low_resolution_dsm. - So, the sensor_to_low_resolution_dsm pipeline produces a refined_conf_full_res.json in the outdir - that contains the epipolar_a_priori information for each sensor image pairs. - The epipolar_a_priori is also saved in the used_conf.json with the sensor_to_full_resolution_dsm pipeline. + The CARS pipeline produces a used_conf.json in the outdir that contains the epipolar_a_priori + information for each sensor image pairs. If you wish to re-run CARS, this time by skipping the + sparse matching, you can use the ``used_conf.json`` as the new input configuration, with + its `use_epipolar_a_priori` parameter set to `True`. For each sensor images, the epipolar a priori are filled as following: @@ -1512,7 +1504,7 @@ The structure follows this organisation: } }, - .. tab:: Outputs + .. tab:: Outputs +------------------+-------------------------------------------------------------+--------------------+----------------------+----------+ diff --git a/setup.cfg b/setup.cfg index 163e6ced..101ec797 100644 --- a/setup.cfg +++ b/setup.cfg @@ -136,7 +136,7 @@ pandora_mccnn = pandora_plugin_mccnn==1.3.0 [options.package_data] - cars = orchestrator/cluster/dask_config/*.yaml, conf/geoid/*.grd, conf/geoid/*.hdr, pipelines/conf_pipeline/*.json, applications/dense_matching/loaders/*.json + cars = orchestrator/cluster/dask_config/*.yaml, conf/geoid/*.grd, conf/geoid/*.hdr, applications/dense_matching/loaders/*.json # cars entry points cli scripts [options.entry_points] diff --git a/tests/applications/resampling/test_resampling.py b/tests/applications/resampling/test_resampling.py index d8763169..45f069a7 100644 --- a/tests/applications/resampling/test_resampling.py +++ b/tests/applications/resampling/test_resampling.py @@ -373,7 +373,6 @@ def test_check_tiles_in_sensor(): _, input_data = generate_input_json( input_json, directory, - "sensors_to_dense_dsm", "local_dask", orchestrator_parameters={ "walltime": "00:10:00", diff --git a/tests/data/ref_output/color_end2end_gizeh_fill.tif b/tests/data/ref_output/color_end2end_gizeh_fill.tif index b48fcaab..0e497535 100644 Binary files a/tests/data/ref_output/color_end2end_gizeh_fill.tif and b/tests/data/ref_output/color_end2end_gizeh_fill.tif differ diff --git a/tests/data/ref_output/dsm_end2end_gizeh_fill.tif b/tests/data/ref_output/dsm_end2end_gizeh_fill.tif index bc48cf48..d0279e39 100644 Binary files a/tests/data/ref_output/dsm_end2end_gizeh_fill.tif and b/tests/data/ref_output/dsm_end2end_gizeh_fill.tif differ diff --git a/tests/data/ref_output/filling_end2end_gizeh_fill.tif b/tests/data/ref_output/filling_end2end_gizeh_fill.tif index 2fd51b76..2909139f 100644 Binary files a/tests/data/ref_output/filling_end2end_gizeh_fill.tif and b/tests/data/ref_output/filling_end2end_gizeh_fill.tif differ diff --git a/tests/data/ref_output/mask_end2end_gizeh_fill.tif b/tests/data/ref_output/mask_end2end_gizeh_fill.tif index 25136bfd..7100a7ca 100644 Binary files a/tests/data/ref_output/mask_end2end_gizeh_fill.tif and b/tests/data/ref_output/mask_end2end_gizeh_fill.tif differ diff --git a/tests/helpers.py b/tests/helpers.py index ed84856c..5cfd3aa8 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -75,7 +75,6 @@ def cars_path(): def generate_input_json( input_json, output_directory, - pipeline, orchestrator_mode, orchestrator_parameters=None, geometry_plugin_name=None, @@ -89,8 +88,6 @@ def generate_input_json( :type input_json: str :param output_directory: absolute path out directory :type output_directory: str - :param pipeline: pipeline to run - :type pipeline: str :param orchestrator_mode: orchestrator mode :type orchestrator_mode: str :param orchestrator_parameters: advanced orchestrator params @@ -117,7 +114,7 @@ def generate_input_json( config["geometry_plugin"] = geometry_plugin_name # overload pipeline - config["pipeline"] = pipeline + config["pipeline"] = "default" # Create keys if "applications" not in config: diff --git a/tests/test_cars.py b/tests/test_cars.py index a346dda0..42882a0c 100644 --- a/tests/test_cars.py +++ b/tests/test_cars.py @@ -97,7 +97,6 @@ def test_low_res_dsm_args(): filled_absolute_path_input, _ = generate_input_json( absolute_data_path("input/phr_ventoux/input.json"), directory, - "sensors_to_sparse_dsm", "sequential", ) args.conf = filled_absolute_path_input @@ -128,7 +127,6 @@ def test_full_res_dsm_args(): filled_absolute_path_input, _ = generate_input_json( absolute_data_path("input/phr_ventoux/input.json"), directory, - "sensors_to_dense_dsm", "sequential", ) args.conf = filled_absolute_path_input diff --git a/tests/test_end2end.py b/tests/test_end2end.py index 01a83115..a13aa490 100644 --- a/tests/test_end2end.py +++ b/tests/test_end2end.py @@ -43,15 +43,7 @@ # CARS imports from cars.core import roi_tools -from cars.pipelines.depth_maps_to_dsm import ( - depth_maps_to_dsm_pipeline as pipeline_dsm, -) -from cars.pipelines.sensor_to_dense_dsm import ( - sensor_to_dense_dsm_pipeline as sensor_to_dense_dsm, -) -from cars.pipelines.sensor_to_sparse_dsm import ( - sensor_to_sparse_dsm_pipeline as sensor_to_sparse_dsm, -) +from cars.pipelines.default import default_pipeline as default # CARS Tests imports from .helpers import ( @@ -82,7 +74,6 @@ def test_end2end_gizeh_rectangle_epi_image_performance_map(): _, input_dense_dsm = generate_input_json( input_json, directory, - "sensors_to_dense_dsm", "multiprocessing", orchestrator_parameters={ "nb_workers": NB_WORKERS, @@ -116,10 +107,9 @@ def test_end2end_gizeh_rectangle_epi_image_performance_map(): "mask": True, "performance_map": True, } + input_dense_dsm["advanced"]["merging"] = True - dense_dsm_pipeline = sensor_to_dense_dsm.SensorToDenseDsmPipeline( - input_dense_dsm - ) + dense_dsm_pipeline = default.DefaultPipeline(input_dense_dsm) dense_dsm_pipeline.run() out_dir = input_dense_dsm["output"]["directory"] @@ -196,11 +186,10 @@ def test_end2end_gizeh_rectangle_epi_image_performance_map(): # launch no mergin pipeline input_dense_dsm["pipeline"] = "sensors_to_dense_dsm_no_merging" + input_dense_dsm["advanced"]["merging"] = False input_dense_dsm["output"]["directory"] = out_dir + "no_merging" - dense_dsm_pipeline = sensor_to_dense_dsm.SensorToDenseDsmPipeline( - input_dense_dsm - ) + dense_dsm_pipeline = default.DefaultPipeline(input_dense_dsm) dense_dsm_pipeline.run() out_dir = input_dense_dsm["output"]["directory"] @@ -237,7 +226,6 @@ def test_end2end_gizeh_rectangle_epi_image_performance_map(): # ) # ), # ) - assert_same_images( os.path.join(out_dir, "dsm", "dsm.tif"), absolute_data_path( @@ -299,7 +287,6 @@ def test_end2end_ventoux_sparse_dsm_8bits(): _, input_config_sparse_dsm = generate_input_json( input_json, directory, - "sensors_to_sparse_dsm", "multiprocessing", orchestrator_parameters={ "nb_workers": NB_WORKERS, @@ -319,13 +306,25 @@ def test_end2end_ventoux_sparse_dsm_8bits(): "elevation_delta_upper_bound": 20.0, "save_intermediate_data": False, }, + "dense_matching": { + # run disp min disp max in the global pipeline + "use_global_disp_range": True + }, + "dem_generation": { + # save the dems in the global pipeline + "save_intermediate_data": True + }, + } + + output_config = { + # reduce computation time by not going further for nothing + "product_level": ["depth_map"] } input_config_sparse_dsm["applications"].update(application_config) + input_config_sparse_dsm["output"].update(output_config) - sparse_res_pipeline = sensor_to_sparse_dsm.SensorSparseDsmPipeline( - input_config_sparse_dsm - ) + sparse_res_pipeline = default.DefaultPipeline(input_config_sparse_dsm) sparse_res_pipeline.run() out_dir = input_config_sparse_dsm["output"]["directory"] @@ -356,11 +355,11 @@ def test_end2end_ventoux_sparse_dsm_8bits(): < -13 ) assert ( - 17 + 7 < out_json["applications"]["left_right"][ "disparity_range_computation_run" ]["maximum_disparity"] - < 22 + < 11 ) used_conf_path = os.path.join(out_dir, "used_conf.json") @@ -442,7 +441,6 @@ def test_end2end_ventoux_unique(): _, input_config_sparse_dsm = generate_input_json( input_json, directory, - "sensors_to_sparse_dsm", "multiprocessing", orchestrator_parameters={ "nb_workers": NB_WORKERS, @@ -450,7 +448,10 @@ def test_end2end_ventoux_unique(): }, ) application_config = { - "grid_generation": {"method": "epipolar", "epi_step": 30}, + "grid_generation": { + "method": "epipolar", + "epi_step": 30, + }, "resampling": {"method": "bicubic", "strip_height": 80}, "sparse_matching": { "method": "sift", @@ -460,13 +461,24 @@ def test_end2end_ventoux_unique(): "disparity_margin": 0.25, "save_intermediate_data": True, }, + "dense_matching": { + # run disp min disp max in the global pipeline + "use_global_disp_range": True + }, + "dem_generation": { + # save the dems in the global pipeline + "save_intermediate_data": True + }, + } + output_config = { + # reduce computation time by not going further for nothing + "product_level": ["depth_map"] } input_config_sparse_dsm["applications"].update(application_config) + input_config_sparse_dsm["output"].update(output_config) - sparse_res_pipeline = sensor_to_sparse_dsm.SensorSparseDsmPipeline( - input_config_sparse_dsm - ) + sparse_res_pipeline = default.DefaultPipeline(input_config_sparse_dsm) sparse_res_pipeline.run() out_dir = input_config_sparse_dsm["output"]["directory"] @@ -490,21 +502,20 @@ def test_end2end_ventoux_unique(): == 612 ) assert ( - -29 + -22 < out_json["applications"]["left_right"][ "disparity_range_computation_run" ]["minimum_disparity"] - < -27 + < -19 ) assert ( - 26 + 12 < out_json["applications"]["left_right"][ "disparity_range_computation_run" ]["maximum_disparity"] - < 30 + < 16 ) - # check matches file exists assert os.path.isfile( out_json["applications"]["left_right"]["grid_correction"][ "corrected_filtered_matches" @@ -592,8 +603,6 @@ def test_end2end_ventoux_unique(): # check used_conf inputs conf exists assert "inputs" in used_conf assert "sensors" in used_conf["inputs"] - # check used_conf pipeline - assert used_conf["pipeline"] == "sensors_to_sparse_dsm" # check used_conf sparse_matching configuration assert ( used_conf["applications"]["sparse_matching"]["disparity_margin"] @@ -606,7 +615,7 @@ def test_end2end_ventoux_unique(): ) # check used_conf reentry - _ = sensor_to_sparse_dsm.SensorSparseDsmPipeline(used_conf) + _ = default.DefaultPipeline(used_conf) # clean outdir shutil.rmtree(out_dir, ignore_errors=False, onerror=None) @@ -701,14 +710,15 @@ def test_end2end_ventoux_unique(): input_config_dense_dsm["applications"].update(dense_dsm_applications) # update epsg input_config_dense_dsm["output"]["epsg"] = 32631 + # update output product + input_config_dense_dsm["output"]["product_level"] = ["dsm"] # resolution input_config_dense_dsm["output"]["resolution"] = 0.5 # update pipeline input_config_dense_dsm["pipeline"] = "sensors_to_dense_dsm" + input_config_dense_dsm["advanced"]["merging"] = True - dense_dsm_pipeline = sensor_to_dense_dsm.SensorToDenseDsmPipeline( - input_config_dense_dsm - ) + dense_dsm_pipeline = default.DefaultPipeline(input_config_dense_dsm) dense_dsm_pipeline.run() out_dir = input_config_sparse_dsm["output"]["directory"] @@ -724,8 +734,6 @@ def test_end2end_ventoux_unique(): # check used_conf inputs conf exists assert "inputs" in used_conf assert "sensors" in used_conf["inputs"] - # check used_conf pipeline - assert used_conf["pipeline"] == "sensors_to_dense_dsm" # check used_conf sparse_matching configuration assert ( used_conf["applications"]["point_cloud_rasterization"]["sigma"] @@ -737,7 +745,7 @@ def test_end2end_ventoux_unique(): == gt_used_conf_orchestrator["orchestrator"] ) # check used_conf reentry - _ = sensor_to_dense_dsm.SensorToDenseDsmPipeline(used_conf) + _ = default.DefaultPipeline(used_conf) # Uncomment the 2 following instructions to update reference data # copy2( @@ -993,7 +1001,6 @@ def test_end2end_ventoux_unique(): _, input_config_sparse_dsm = generate_input_json( input_json, directory, - "sensors_to_sparse_dsm", "multiprocessing", orchestrator_parameters={ "nb_workers": NB_WORKERS, @@ -1011,13 +1018,172 @@ def test_end2end_ventoux_unique(): "disparity_margin": 0.25, "save_intermediate_data": True, }, + "dense_matching": { + # run disp min disp max in the global pipeline + "use_global_disp_range": True + }, + "dem_generation": { + # save the dems in the global pipeline + "save_intermediate_data": True + }, } input_config_sparse_dsm["applications"].update(application_config) - sparse_res_pipeline = sensor_to_sparse_dsm.SensorSparseDsmPipeline( - input_config_sparse_dsm + sparse_res_pipeline = default.DefaultPipeline(input_config_sparse_dsm) + sparse_res_pipeline.run() + + out_dir = input_config_sparse_dsm["output"]["directory"] + + # clean outdir + shutil.rmtree(out_dir, ignore_errors=False, onerror=None) + + # dense dsm pipeline + input_config_dense_dsm = input_config_sparse_dsm.copy() + # update applications + dense_dsm_applications = { + "dense_matching": { + "method": "census_sgm", + "use_global_disp_range": False, + "loader_conf": { + "input": {}, + "pipeline": { + "matching_cost": { + "matching_cost_method": "census", + "window_size": 5, + "subpix": 1, + }, + "cost_volume_confidence.before": { + "confidence_method": "ambiguity", + "eta_max": 0.7, + "eta_step": 0.01, + }, + "cost_volume_confidence.std_intensity_before": { + "confidence_method": "std_intensity" + }, + "cost_volume_confidence.risk_before": { + "confidence_method": "risk" + }, + "optimization": { + "optimization_method": "sgm", + "penalty": { + "P1": 8, + "P2": 32, + "p2_method": "constant", + "penalty_method": "sgm_penalty", + }, + "overcounting": False, + "min_cost_paths": False, + }, + "cost_volume_confidence": { + "confidence_method": "ambiguity", + "eta_max": 0.7, + "eta_step": 0.01, + }, + "cost_volume_confidence.std_intensity": { + "confidence_method": "std_intensity" + }, + "cost_volume_confidence.risk": { + "confidence_method": "risk" + }, + "disparity": { + "disparity_method": "wta", + "invalid_disparity": "NaN", + }, + "refinement": {"refinement_method": "vfit"}, + "filter": {"filter_method": "median", "filter_size": 3}, + "validation": { + "validation_method": "cross_checking_accurate", + "cross_checking_threshold": 1.0, + }, + }, + }, + }, + "point_cloud_outliers_removing.1": { + "method": "small_components", + "activated": True, + }, + "point_cloud_outliers_removing.2": { + "method": "statistical", + "activated": True, + }, + "point_cloud_rasterization": { + "method": "simple_gaussian", + "dsm_radius": 3, + "sigma": 0.3, + "dsm_no_data": -999, + "color_no_data": 0, + }, + } + input_config_dense_dsm["applications"].update(dense_dsm_applications) + # update epsg + input_config_dense_dsm["output"]["epsg"] = 32631 + # resolution + input_config_dense_dsm["output"]["resolution"] = 0.5 + # update pipeline + input_config_dense_dsm["pipeline"] = "sensors_to_dense_dsm" + input_config_dense_dsm["advanced"]["merging"] = True + + dense_dsm_pipeline = default.DefaultPipeline(input_config_dense_dsm) + dense_dsm_pipeline.run() + + out_dir = input_config_sparse_dsm["output"]["directory"] + + assert_same_images( + os.path.join(out_dir, "dsm", "dsm.tif"), + absolute_data_path( + os.path.join(ref_output_dir, "dsm_end2end_ventoux.tif") + ), + atol=0.0001, + rtol=1e-6, ) + assert_same_images( + os.path.join(out_dir, "dsm", "color.tif"), + absolute_data_path( + os.path.join(ref_output_dir, "color_end2end_ventoux.tif") + ), + rtol=0.0002, + atol=1.0e-6, + ) + assert os.path.exists(os.path.join(out_dir, "mask.tif")) is False + + # Test we have the same results with multiprocessing + with tempfile.TemporaryDirectory(dir=temporary_dir()) as directory: + input_json = absolute_data_path("input/phr_ventoux/input.json") + # Run sparse dsm pipeline + _, input_config_sparse_dsm = generate_input_json( + input_json, + directory, + "mp", + orchestrator_parameters={ + "nb_workers": 4, + "max_ram_per_worker": 1000, + }, + ) + application_config = { + "grid_generation": {"method": "epipolar", "epi_step": 30}, + "resampling": {"method": "bicubic", "strip_height": 80}, + "sparse_matching": { + "method": "sift", + "epipolar_error_upper_bound": 43.0, + "elevation_delta_lower_bound": -20.0, + "elevation_delta_upper_bound": 20.0, + "disparity_margin": 0.25, + "save_intermediate_data": True, + }, + "dense_matching": { + # run disp min disp max in the global pipeline + "use_global_disp_range": True + }, + "dem_generation": { + # save the dems in the global pipeline + "save_intermediate_data": True + }, + } + + input_config_sparse_dsm["applications"].update(application_config) + + sparse_res_pipeline = default.DefaultPipeline(input_config_sparse_dsm) sparse_res_pipeline.run() out_dir = input_config_sparse_dsm["output"]["directory"] @@ -1057,10 +1223,9 @@ def test_end2end_ventoux_unique(): input_config_dense_dsm["output"]["resolution"] = 0.5 # update pipeline input_config_dense_dsm["pipeline"] = "sensors_to_dense_dsm" + input_config_dense_dsm["advanced"]["merging"] = True - dense_dsm_pipeline = sensor_to_dense_dsm.SensorToDenseDsmPipeline( - input_config_dense_dsm - ) + dense_dsm_pipeline = default.DefaultPipeline(input_config_dense_dsm) dense_dsm_pipeline.run() out_dir = input_config_sparse_dsm["output"]["directory"] @@ -1098,7 +1263,6 @@ def test_end2end_ventoux_unique_split_epsg_4326(): _, input_config_pc = generate_input_json( input_json, directory, - "sensors_to_dense_depth_maps", "multiprocessing", orchestrator_parameters={ "nb_workers": NB_WORKERS, @@ -1111,9 +1275,8 @@ def test_end2end_ventoux_unique_split_epsg_4326(): "use_cross_validation": True, }, } - pc_pipeline = sensor_to_dense_dsm.SensorToDenseDsmPipeline( - input_config_pc - ) + input_config_pc["output"]["product_level"] = ["depth_map"] + pc_pipeline = default.DefaultPipeline(input_config_pc) input_config_pc["output"]["epsg"] = 4326 @@ -1172,9 +1335,10 @@ def test_end2end_ventoux_unique_split_epsg_4326(): "save_intermediate_data": True, } }, + "advanced": {"merging": True}, } - dsm_pipeline = pipeline_dsm.DepthMapsToDsmPipeline(input_dsm_config) + dsm_pipeline = default.DefaultPipeline(input_dsm_config) dsm_pipeline.run() out_dir_dsm = input_dsm_config["output"]["directory"] @@ -1245,12 +1409,11 @@ def test_end2end_ventoux_unique_split_epsg_4326(): ) # launch with no merging pipeline - input_dsm_config["pipeline"] = ( - "dense_depth_maps_to_dense_dsm_no_merging" - ) + + input_dsm_config["advanced"]["merging"] = False input_dsm_config["output"]["directory"] = output_path + "no_merging" - dsm_pipeline = pipeline_dsm.DepthMapsToDsmPipeline(input_dsm_config) + dsm_pipeline = default.DefaultPipeline(input_dsm_config) dsm_pipeline.run() out_dir_dsm = input_dsm_config["output"]["directory"] @@ -1306,7 +1469,6 @@ def test_end2end_ventoux_unique_split(): _, input_config_pc = generate_input_json( input_json, directory, - "sensors_to_dense_depth_maps", "multiprocessing", orchestrator_parameters={ "nb_workers": NB_WORKERS, @@ -1384,6 +1546,7 @@ def test_end2end_ventoux_unique_split(): input_config_pc["applications"].update(application_config) + input_config_pc["output"]["product_level"] = ["depth_map"] input_config_pc["output"]["auxiliary"] = { "classification": True, "filling": True, @@ -1391,9 +1554,7 @@ def test_end2end_ventoux_unique_split(): "performance_map": True, } - pc_pipeline = sensor_to_dense_dsm.SensorToDenseDsmPipeline( - input_config_pc - ) + pc_pipeline = default.DefaultPipeline(input_config_pc) pc_pipeline.run() out_dir = input_config_pc["output"]["directory"] @@ -1482,9 +1643,10 @@ def test_end2end_ventoux_unique_split(): "save_intermediate_data": True, }, }, + "advanced": {"merging": True}, } - dsm_pipeline = pipeline_dsm.DepthMapsToDsmPipeline(input_dsm_config) + dsm_pipeline = default.DefaultPipeline(input_dsm_config) dsm_pipeline.run() out_dir_dsm = input_dsm_config["output"]["directory"] @@ -1718,9 +1880,17 @@ def test_end2end_ventoux_unique_split(): # Run no merging pipeline + # avoid deleting other variables in advanced if it exists + if "advanced" in input_dsm_config: + input_dsm_config["advanced"]["merging"] = False + else: + input_dsm_config["advanced"] = {"merging": False} + + input_dsm_config["output"]["product_level"] = ["dsm"] input_dsm_config["output"]["directory"] = ( input_dsm_config["output"]["directory"] + "_no_merging" ) + del input_dsm_config["applications"][ "point_cloud_outliers_removing.1" ] @@ -1730,9 +1900,10 @@ def test_end2end_ventoux_unique_split(): input_dsm_config["pipeline"] = ( "dense_depth_maps_to_dense_dsm_no_merging" ) + input_dsm_config["advanced"]["merging"] = False # launch - dsm_pipeline = pipeline_dsm.DepthMapsToDsmPipeline(input_dsm_config) + dsm_pipeline = default.DefaultPipeline(input_dsm_config) dsm_pipeline.run() out_dir_dsm = input_dsm_config["output"]["directory"] @@ -1973,7 +2144,6 @@ def test_end2end_use_epipolar_a_priori(): _, input_config_sparse_res = generate_input_json( input_json, directory, - "sensors_to_sparse_dsm", "multiprocessing", orchestrator_parameters={ "nb_workers": NB_WORKERS, @@ -1992,12 +2162,23 @@ def test_end2end_use_epipolar_a_priori(): "disparity_margin": 0.25, "save_intermediate_data": True, }, + "dense_matching": { + # run disp min disp max in the global pipeline + "use_global_disp_range": True + }, + "dem_generation": { + # save the dems in the global pipeline + "save_intermediate_data": True + }, + } + output_config = { + # reduce computation time by not going further for nothing + "product_level": ["depth_map"] } + input_config_sparse_res["output"].update(output_config) input_config_sparse_res["applications"].update(application_config) - sparse_res_pipeline = sensor_to_sparse_dsm.SensorSparseDsmPipeline( - input_config_sparse_res - ) + sparse_res_pipeline = default.DefaultPipeline(input_config_sparse_res) sparse_res_pipeline.run() out_dir = input_config_sparse_res["output"]["directory"] @@ -2143,8 +2324,6 @@ def test_end2end_use_epipolar_a_priori(): # check used_conf inputs conf exists assert "inputs" in used_conf assert "sensors" in used_conf["inputs"] - # check used_conf pipeline - assert used_conf["pipeline"] == "sensors_to_sparse_dsm" # check used_conf sparse_matching configuration assert ( used_conf["applications"]["sparse_matching"]["disparity_margin"] @@ -2156,7 +2335,7 @@ def test_end2end_use_epipolar_a_priori(): == gt_used_conf_orchestrator["orchestrator"] ) # check used_conf reentry - _ = sensor_to_sparse_dsm.SensorSparseDsmPipeline(used_conf) + _ = default.DefaultPipeline(used_conf) refined_config_dense_dsm_json = os.path.join( out_dir, "refined_config_dense_dsm.json" @@ -2169,11 +2348,6 @@ def test_end2end_use_epipolar_a_priori(): # check refined_config_dense_dsm_json inputs conf exists assert "inputs" in refined_config_dense_dsm_json assert "sensors" in refined_config_dense_dsm_json["inputs"] - # check refined_config_dense_dsm_json pipeline - assert ( - refined_config_dense_dsm_json["pipeline"] - == "sensors_to_dense_dsm" - ) # check refined_config_dense_dsm_json sparse_matching configuration assert "advanced" in refined_config_dense_dsm_json assert ( @@ -2245,9 +2419,10 @@ def test_end2end_use_epipolar_a_priori(): input_config_dense_dsm["output"]["resolution"] = 0.5 # Update outdir, write new dir input_config_dense_dsm["output"]["directory"] += "dense" - dense_dsm_pipeline = sensor_to_dense_dsm.SensorToDenseDsmPipeline( - input_config_dense_dsm - ) + + input_config_dense_dsm["advanced"]["merging"] = True + + dense_dsm_pipeline = default.DefaultPipeline(input_config_dense_dsm) dense_dsm_pipeline.run() @@ -2264,8 +2439,6 @@ def test_end2end_use_epipolar_a_priori(): # check used_conf inputs conf exists assert "inputs" in used_conf assert "sensors" in used_conf["inputs"] - # check used_conf pipeline - assert used_conf["pipeline"] == "sensors_to_dense_dsm" # check used_conf sparse_matching configuration assert ( used_conf["applications"]["point_cloud_rasterization"]["sigma"] @@ -2277,7 +2450,7 @@ def test_end2end_use_epipolar_a_priori(): == gt_used_conf_orchestrator["orchestrator"] ) # check used_conf reentry - _ = sensor_to_dense_dsm.SensorToDenseDsmPipeline(used_conf) + _ = default.DefaultPipeline(used_conf) # Ref output dir dependent from geometry plugin chosen ref_output_dir = "ref_output" @@ -2357,7 +2530,6 @@ def test_prepare_ventoux_bias(): _, input_config_sparse_res = generate_input_json( input_json, directory, - "sensors_to_sparse_dsm", "multiprocessing", orchestrator_parameters={ "nb_workers": NB_WORKERS, @@ -2376,13 +2548,25 @@ def test_prepare_ventoux_bias(): "disparity_margin": 0.25, "save_intermediate_data": True, }, + "dense_matching": { + # run disp min disp max in the global pipeline + "use_global_disp_range": True + }, + "dem_generation": { + # save the dems in the global pipeline + "save_intermediate_data": True + }, + } + + output_config = { + # reduce computation time by not going further for nothing + "product_level": ["depth_map"] } input_config_sparse_res["applications"].update(application_config) + input_config_sparse_res["output"].update(output_config) - sparse_res_pipeline = sensor_to_sparse_dsm.SensorSparseDsmPipeline( - input_config_sparse_res - ) + sparse_res_pipeline = default.DefaultPipeline(input_config_sparse_res) sparse_res_pipeline.run() out_dir = input_config_sparse_res["output"]["directory"] @@ -2401,10 +2585,10 @@ def test_prepare_ventoux_bias(): out_disp_compute = out_data["applications"]["left_right"][ "disparity_range_computation_run" ] - assert out_disp_compute["minimum_disparity"] > -28 - assert out_disp_compute["minimum_disparity"] < -26 - assert out_disp_compute["maximum_disparity"] > 24 - assert out_disp_compute["maximum_disparity"] < 29 + assert out_disp_compute["minimum_disparity"] > -86 + assert out_disp_compute["minimum_disparity"] < -83 + assert out_disp_compute["maximum_disparity"] > -48 + assert out_disp_compute["maximum_disparity"] < -46 # check matches file exists assert os.path.isfile( @@ -2430,7 +2614,6 @@ def test_end2end_ventoux_full_output_no_elevation(): _, input_config = generate_input_json( input_json, directory, - "sensors_to_dense_dsm", "multiprocessing", orchestrator_parameters={ "nb_workers": NB_WORKERS, @@ -2476,8 +2659,9 @@ def test_end2end_ventoux_full_output_no_elevation(): input_config["applications"].update(application_config) input_config["advanced"].update(advanced_config) input_config["output"].update(output_config) + input_config["advanced"]["merging"] = True - pipeline = sensor_to_dense_dsm.SensorToDenseDsmPipeline(input_config) + pipeline = default.DefaultPipeline(input_config) pipeline.run() @@ -2779,7 +2963,6 @@ def test_end2end_ventoux_with_color(): _, input_config_sparse_res = generate_input_json( input_json, directory, - "sensors_to_sparse_dsm", "multiprocessing", orchestrator_parameters={ "nb_workers": NB_WORKERS, @@ -2801,13 +2984,25 @@ def test_end2end_ventoux_with_color(): "disparity_margin": 0.25, "save_intermediate_data": True, }, + "dense_matching": { + # run disp min disp max in the global pipeline + "use_global_disp_range": True + }, + "dem_generation": { + # save the dems in the global pipeline + "save_intermediate_data": True + }, + } + + output_config = { + # reduce computation time by not going further for nothing + "product_level": ["depth_map"] } input_config_sparse_res["applications"].update(application_config) + input_config_sparse_res["output"].update(output_config) - sparse_res_pipeline = sensor_to_sparse_dsm.SensorSparseDsmPipeline( - input_config_sparse_res - ) + sparse_res_pipeline = default.DefaultPipeline(input_config_sparse_res) sparse_res_pipeline.run() out_dir = input_config_sparse_res["output"]["directory"] @@ -2826,10 +3021,10 @@ def test_end2end_ventoux_with_color(): out_disp_compute = out_data["applications"]["left_right"][ "disparity_range_computation_run" ] - assert out_disp_compute["minimum_disparity"] > -29 - assert out_disp_compute["minimum_disparity"] < -27 - assert out_disp_compute["maximum_disparity"] > 26 - assert out_disp_compute["maximum_disparity"] < 30 + assert out_disp_compute["minimum_disparity"] > -20 + assert out_disp_compute["minimum_disparity"] < -18 + assert out_disp_compute["maximum_disparity"] > 13 + assert out_disp_compute["maximum_disparity"] < 15 assert os.path.isfile( out_data["applications"]["left_right"]["grid_correction"][ @@ -2892,13 +3087,13 @@ def test_end2end_ventoux_with_color(): # update pipeline input_config_dense_dsm["pipeline"] = "sensors_to_dense_dsm" + input_config_dense_dsm["output"]["product_level"] = ["dsm"] + input_config_dense_dsm["advanced"]["merging"] = True - dense_dsm_pipeline = sensor_to_dense_dsm.SensorToDenseDsmPipeline( - input_config_dense_dsm - ) + dense_dsm_pipeline = default.DefaultPipeline(input_config_dense_dsm) dense_dsm_pipeline.run() - out_dir = input_config_sparse_res["output"]["directory"] + out_dir = input_config_dense_dsm["output"]["directory"] assert ( os.path.exists( @@ -3040,7 +3235,6 @@ def test_end2end_ventoux_with_classif(): _, input_config_sparse_res = generate_input_json( input_json, directory, - "sensors_to_sparse_dsm", "multiprocessing", orchestrator_parameters={ "nb_workers": NB_WORKERS, @@ -3062,13 +3256,25 @@ def test_end2end_ventoux_with_classif(): "disparity_margin": 0.25, "save_intermediate_data": True, }, + "dense_matching": { + # run disp min disp max in the global pipeline + "use_global_disp_range": True + }, + "dem_generation": { + # save the dems in the global pipeline + "save_intermediate_data": True + }, + } + + output_config = { + # reduce computation time by not going further for nothing + "product_level": ["depth_map"] } input_config_sparse_res["applications"].update(application_config) + input_config_sparse_res["output"].update(output_config) - sparse_res_pipeline = sensor_to_sparse_dsm.SensorSparseDsmPipeline( - input_config_sparse_res - ) + sparse_res_pipeline = default.DefaultPipeline(input_config_sparse_res) sparse_res_pipeline.run() out_dir = input_config_sparse_res["output"]["directory"] @@ -3087,10 +3293,10 @@ def test_end2end_ventoux_with_classif(): out_disp_compute = out_data["applications"]["left_right"][ "disparity_range_computation_run" ] - assert out_disp_compute["minimum_disparity"] > -29 - assert out_disp_compute["minimum_disparity"] < -27 - assert out_disp_compute["maximum_disparity"] > 26 - assert out_disp_compute["maximum_disparity"] < 30 + assert out_disp_compute["minimum_disparity"] > -20 + assert out_disp_compute["minimum_disparity"] < -18 + assert out_disp_compute["maximum_disparity"] > 13 + assert out_disp_compute["maximum_disparity"] < 15 assert os.path.isfile( out_data["applications"]["left_right"]["grid_correction"][ @@ -3146,15 +3352,12 @@ def test_end2end_ventoux_with_classif(): # update epsg input_config_dense_dsm["output"]["resolution"] = 0.5 + input_config_dense_dsm["output"]["product_level"] = ["dsm"] + # Save classif input_config_dense_dsm["output"]["auxiliary"] = {"classification": True} - - # update pipeline - input_config_dense_dsm["pipeline"] = "sensors_to_dense_dsm" - - dense_dsm_pipeline = sensor_to_dense_dsm.SensorToDenseDsmPipeline( - input_config_dense_dsm - ) + input_config_dense_dsm["advanced"]["merging"] = True + dense_dsm_pipeline = default.DefaultPipeline(input_config_dense_dsm) dense_dsm_pipeline.run() out_dir = input_config_sparse_res["output"]["directory"] @@ -3279,7 +3482,6 @@ def test_compute_dsm_with_roi_ventoux(): _, input_config_dense_dsm = generate_input_json( input_json, directory, - "sensors_to_dense_dsm", "multiprocessing", orchestrator_parameters={ "nb_workers": NB_WORKERS, @@ -3345,9 +3547,7 @@ def test_compute_dsm_with_roi_ventoux(): input_config_dense_dsm["inputs"]["roi"] = roi_geo_json - dense_dsm_pipeline = sensor_to_dense_dsm.SensorToDenseDsmPipeline( - input_config_dense_dsm - ) + dense_dsm_pipeline = default.DefaultPipeline(input_config_dense_dsm) dense_dsm_pipeline.run() out_dir = input_config_dense_dsm["output"]["directory"] @@ -3433,7 +3633,6 @@ def test_compute_dsm_with_snap_to_img1(): _, input_config_dense_dsm = generate_input_json( input_json, directory, - "sensors_to_dense_dsm", "multiprocessing", orchestrator_parameters={ "nb_workers": NB_WORKERS, @@ -3485,10 +3684,9 @@ def test_compute_dsm_with_snap_to_img1(): # resolution resolution = 0.5 input_config_dense_dsm["output"]["resolution"] = resolution + input_config_dense_dsm["advanced"]["merging"] = True - dense_dsm_pipeline = sensor_to_dense_dsm.SensorToDenseDsmPipeline( - input_config_dense_dsm - ) + dense_dsm_pipeline = default.DefaultPipeline(input_config_dense_dsm) dense_dsm_pipeline.run() out_dir = input_config_dense_dsm["output"]["directory"] @@ -3553,7 +3751,6 @@ def test_end2end_quality_stats(): _, input_config_dense_dsm = generate_input_json( input_json, directory, - "sensors_to_dense_dsm", "multiprocessing", orchestrator_parameters={ "nb_workers": NB_WORKERS, @@ -3602,12 +3799,13 @@ def test_end2end_quality_stats(): resolution = 0.5 input_config_dense_dsm["output"]["resolution"] = resolution - # Save all intermediate data - input_config_dense_dsm["advanced"] = {"save_intermediate_data": True} + # Save all intermediate data and add merging + input_config_dense_dsm["advanced"] = { + "save_intermediate_data": True, + "merging": True, + } - dense_dsm_pipeline = sensor_to_dense_dsm.SensorToDenseDsmPipeline( - input_config_dense_dsm - ) + dense_dsm_pipeline = default.DefaultPipeline(input_config_dense_dsm) dense_dsm_pipeline.run() out_dir = input_config_dense_dsm["output"]["directory"] @@ -3843,7 +4041,6 @@ def test_end2end_ventoux_egm96_geoid(): _, input_config_dense_dsm = generate_input_json( input_json, directory, - "sensors_to_dense_dsm", "multiprocessing", orchestrator_parameters={ "nb_workers": NB_WORKERS, @@ -3897,10 +4094,9 @@ def test_end2end_ventoux_egm96_geoid(): input_config_dense_dsm["output"]["resolution"] = resolution input_config_dense_dsm["output"]["geoid"] = True + input_config_dense_dsm["advanced"]["merging"] = True - dense_dsm_pipeline = sensor_to_dense_dsm.SensorToDenseDsmPipeline( - input_config_dense_dsm - ) + dense_dsm_pipeline = default.DefaultPipeline(input_config_dense_dsm) dense_dsm_pipeline.run() out_dir = input_config_dense_dsm["output"]["directory"] @@ -3976,7 +4172,6 @@ def test_end2end_ventoux_egm96_geoid(): _, input_config_dense_dsm = generate_input_json( input_json, directory, - "sensors_to_dense_dsm", "multiprocessing", orchestrator_parameters={ "nb_workers": NB_WORKERS, @@ -4031,10 +4226,9 @@ def test_end2end_ventoux_egm96_geoid(): input_config_dense_dsm["output"]["resolution"] = resolution input_config_dense_dsm["output"]["geoid"] = True + input_config_dense_dsm["advanced"]["merging"] = True - dense_dsm_pipeline = sensor_to_dense_dsm.SensorToDenseDsmPipeline( - input_config_dense_dsm - ) + dense_dsm_pipeline = default.DefaultPipeline(input_config_dense_dsm) dense_dsm_pipeline.run() out_dir = input_config_dense_dsm["output"]["directory"] @@ -4067,7 +4261,6 @@ def test_end2end_ventoux_egm96_geoid(): _, input_config_dense_dsm = generate_input_json( input_json, directory, - "sensors_to_dense_dsm", "multiprocessing", orchestrator_parameters={ "nb_workers": NB_WORKERS, @@ -4123,10 +4316,9 @@ def test_end2end_ventoux_egm96_geoid(): input_config_dense_dsm["output"]["geoid"] = absolute_data_path( "input/geoid/egm96_15_modified.tif" ) + input_config_dense_dsm["advanced"]["merging"] = True - dense_dsm_pipeline = sensor_to_dense_dsm.SensorToDenseDsmPipeline( - input_config_dense_dsm - ) + dense_dsm_pipeline = default.DefaultPipeline(input_config_dense_dsm) dense_dsm_pipeline.run() out_dir = input_config_dense_dsm["output"]["directory"] @@ -4214,7 +4406,6 @@ def test_end2end_paca_with_mask(): _, input_config_dense_dsm = generate_input_json( input_json, directory, - "sensors_to_dense_dsm", "multiprocessing", orchestrator_parameters={ "nb_workers": NB_WORKERS, @@ -4267,10 +4458,9 @@ def test_end2end_paca_with_mask(): input_config_dense_dsm["output"]["auxiliary"] = {"mask": True} resolution = 0.5 input_config_dense_dsm["output"]["resolution"] = resolution + input_config_dense_dsm["advanced"]["merging"] = True - dense_dsm_pipeline = sensor_to_dense_dsm.SensorToDenseDsmPipeline( - input_config_dense_dsm - ) + dense_dsm_pipeline = default.DefaultPipeline(input_config_dense_dsm) dense_dsm_pipeline.run() out_dir = input_config_dense_dsm["output"]["directory"] @@ -4337,7 +4527,6 @@ def test_end2end_disparity_filling(): _, input_config_dense_dsm = generate_input_json( input_json, directory, - "sensors_to_dense_dsm", "multiprocessing", orchestrator_parameters={ "nb_workers": NB_WORKERS, @@ -4388,10 +4577,9 @@ def test_end2end_disparity_filling(): "filling": True, "mask": True, } + input_config_dense_dsm["advanced"]["merging"] = True - dense_dsm_pipeline = sensor_to_dense_dsm.SensorToDenseDsmPipeline( - input_config_dense_dsm - ) + dense_dsm_pipeline = default.DefaultPipeline(input_config_dense_dsm) dense_dsm_pipeline.run() out_dir = input_config_dense_dsm["output"]["directory"] @@ -4474,7 +4662,6 @@ def test_end2end_disparity_filling_with_zeros(): _, input_config_dense_dsm = generate_input_json( input_json, directory, - "sensors_to_dense_dsm", "multiprocessing", ) dense_dsm_applications = { @@ -4510,9 +4697,9 @@ def test_end2end_disparity_filling_with_zeros(): "mask": True, } - dense_dsm_pipeline = sensor_to_dense_dsm.SensorToDenseDsmPipeline( - input_config_dense_dsm - ) + input_config_dense_dsm["advanced"]["merging"] = True + + dense_dsm_pipeline = default.DefaultPipeline(input_config_dense_dsm) dense_dsm_pipeline.run() out_dir = input_config_dense_dsm["output"]["directory"] @@ -4614,7 +4801,6 @@ def test_end2end_gizeh_dry_run_of_used_conf(): _, sensors_input_config_first_run = generate_input_json( input_json, directory, - "sensors_to_dense_dsm", "multiprocessing", ) @@ -4626,10 +4812,8 @@ def test_end2end_gizeh_dry_run_of_used_conf(): sensors_input_config_first_run["applications"].update(applications) - sensors_pipeline_first_run = ( - sensor_to_dense_dsm.SensorToDenseDsmPipeline( - sensors_input_config_first_run - ) + sensors_pipeline_first_run = default.DefaultPipeline( + sensors_input_config_first_run ) sensors_pipeline_first_run.run() sensors_out_dir_first_run = sensors_input_config_first_run["output"][ @@ -4645,10 +4829,8 @@ def test_end2end_gizeh_dry_run_of_used_conf(): "directory" ] += "_from_used_conf" - sensors_pipeline_second_run = ( - sensor_to_dense_dsm.SensorToDenseDsmPipeline( - sensors_input_config_second_run - ) + sensors_pipeline_second_run = default.DefaultPipeline( + sensors_input_config_second_run ) sensors_pipeline_second_run.run() sensors_out_dir_second_run = sensors_input_config_second_run["output"][ @@ -4684,7 +4866,7 @@ def test_end2end_gizeh_dry_run_of_used_conf(): "output": {"directory": directory2}, } - pc_pipeline_first_run = pipeline_dsm.DepthMapsToDsmPipeline( + pc_pipeline_first_run = default.DefaultPipeline( pc_input_config_first_run ) pc_pipeline_first_run.run() @@ -4701,7 +4883,7 @@ def test_end2end_gizeh_dry_run_of_used_conf(): "directory" ] += "_from_used_conf" - pc_pipeline_second_run = pipeline_dsm.DepthMapsToDsmPipeline( + pc_pipeline_second_run = default.DefaultPipeline( pc_input_config_second_run ) pc_pipeline_second_run.run()