From cfc66e5b0fbcab953cce80021ed39264857b14c4 Mon Sep 17 00:00:00 2001 From: ylannl Date: Fri, 17 Jul 2026 17:08:01 +0200 Subject: [PATCH 1/8] Consolidate parameter handling, expose almost all reconstruction stage parameters in config file. --- CHANGELOG.md | 36 +- apps/roofer-app/config.hpp | 359 ++++++++++++------ apps/roofer-app/crop_tile.hpp | 9 +- apps/roofer-app/doc-helper.cpp | 44 ++- apps/roofer-app/parameter.hpp | 146 +++++-- apps/roofer-app/reconstruct_building.hpp | 77 ++-- apps/roofer-app/roofer-app.cpp | 6 +- apps/roofer-app/validators.hpp | 61 +-- docs/reconstruct_params.md | 29 +- include/roofer/common/ConfigField.hpp | 167 ++++++++ include/roofer/reconstruction/AlphaShaper.hpp | 19 +- .../reconstruction/ArrangementBuilder.hpp | 20 +- .../reconstruction/ArrangementDissolver.hpp | 24 +- .../reconstruction/ArrangementExtruder.hpp | 29 +- .../reconstruction/ArrangementOptimiser.hpp | 35 +- .../reconstruction/ArrangementSnapper.hpp | 21 +- .../roofer/reconstruction/LineDetector.hpp | 31 +- .../roofer/reconstruction/LineRegulariser.hpp | 17 +- .../reconstruction/MeshTriangulator.hpp | 13 +- .../roofer/reconstruction/PlaneDetector.hpp | 76 ++-- .../reconstruction/PlaneIntersector.hpp | 22 +- .../reconstruction/ReconstructionConfig.hpp | 124 ++++++ .../reconstruction/SegmentRasteriser.hpp | 22 +- include/roofer/roofer.h | 175 ++++++--- rooferpy/rooferpy.cpp | 238 ++++++++++-- src/core/reconstruction/AlphaShaper.cpp | 8 +- .../reconstruction/ArrangementBuilder.cpp | 6 +- .../reconstruction/ArrangementDissolver.cpp | 4 +- .../reconstruction/ArrangementExtruder.cpp | 10 +- .../reconstruction/ArrangementOptimiser.cpp | 10 +- .../reconstruction/ArrangementSnapper.cpp | 2 +- src/core/reconstruction/LineDetector.cpp | 14 +- src/core/reconstruction/LineRegulariser.cpp | 2 +- .../reconstruction/MeshTriangulatorLegacy.cpp | 2 +- src/core/reconstruction/PlaneDetector.cpp | 63 ++- src/core/reconstruction/PlaneIntersector.cpp | 16 +- src/core/reconstruction/SegmentRasteriser.cpp | 4 +- tests/CMakeLists.txt | 19 + tests/test_app_reconstruction_config.cpp | 204 ++++++++++ tests/test_arrangement_dissolver.cpp | 4 +- tests/test_arrangement_snapper.cpp | 22 +- tests/test_reconstruct_api.cpp | 33 +- tests/test_reconstruction_config.cpp | 99 +++++ 43 files changed, 1810 insertions(+), 512 deletions(-) create mode 100644 include/roofer/common/ConfigField.hpp create mode 100644 include/roofer/reconstruction/ReconstructionConfig.hpp create mode 100644 tests/test_app_reconstruction_config.cpp create mode 100644 tests/test_reconstruction_config.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index d3ef0178..7ef1051b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Repair of non-manifold and self-intersecting roof junctions during arrangement snapping. This prevents certain geometric errors like non-watertight meshes, and highly non-planar faces that could prevriously happen in complex roof arrangements. See #168. - Optional per-tile triangulated terrain output as CityJSON `TINRelief` features, see the `--terrain` option. - A new `max-nodata-fraction` configuration option to control the maximum fraction of nodata pixels in a building polygon that is allowed before a building pointcloud is considered insufficient. +- A descriptor-backed `roofer::reconstruction::ReconstructionConfig` that contains the configuration for every reconstruction stage. Field declarations are now the single source for defaults, descriptions, validation, nested TOML parsing, generated documentation, and Python bindings. +- A nested C++ `ReconstructOptions` API and corresponding `reconstruct` overload. The nested reconstruction configuration is also available from Python, including each component configuration. ## Fixed - More robust handling of OGC WKT payloads in LAS/LAZ files. @@ -19,8 +21,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Changed - The pointcloud-insufficient test is now an absolute per-building density floor (`min-building-density`, points/m²) instead of a tile-relative `mean - 2·std` outlier test. The old test depended the entire tile that was being processed, so the same building could be flagged differently between runs with different tiling — potentially skipping point-rich buildings and leaving them without geometry. The decision is now deterministic and depends only on the building's own data. -- Region growing no longer uses a wall-clock time limit, which could make a building reconstruct with success on one run and fall back to LoD 1.1 on the next given identical input due to resource starvation with multithreading. The deterministic plane-count limit (`lod11-fallback-planes`) is now the sole reconstruction-complexity cutoff. The `lod11-fallback-time` parameter is deprecated and removed. -- Use mesh centroid for volume calculation +- Region growing no longer uses a wall-clock time limit, which could make a building reconstruct with success on one run and fall back to LoD 1.1 on the next given identical input due to resource starvation with multithreading. The deterministic plane-count limit (`lod11-fallback-planes`) is now the sole reconstruction-complexity cutoff. The `lod11-fallback-time` behavior is removed; its CLI flag and root configuration key are accepted but ignored with a deprecation warning during 1.x compatibility. +- Reconstruction configuration now uses nested TOML tables such as `[reconstruction.plane-detector]` and `[reconstruction.arrangement-optimiser]`. Complete component configurations are passed through the pipeline; pipeline-derived settings such as requested LoD and terrain availability are overlaid on local copies. +- Reconstruction parameter descriptions now state physical and angular units where applicable; counts remain unit-free. +- Descriptor-generated TOML documentation and Python bindings omit reconstruction components that have no public parameters. +- `complexity-factor` now directly controls the optimiser energy balance: the data term is weighted by `complexity-factor` and the smoothness term by `1 - complexity-factor`. The independent `data_multiplier` and `smoothness_multiplier` component options were removed. +- The flattened C++ `roofer::ReconstructionConfig` and its `reconstruct` overload remain as deprecated 1.x adapters. Their defaults and validation delegate to the nested configuration. +- Legacy root-level reconstruction TOML keys remain accepted during 1.x, emit a destination-specific deprecation warning, and are applied before nested TOML values. CLI arguments retain the highest precedence. The `--lod12`, `--lod13`, `--lod22`, `--clip-terrain`, and `--complexity-factor` flags remain supported without deprecation warnings; other legacy reconstruction CLI flags are deprecated. +- Use mesh centroid for volume calculation. + +### Reconstruction parameter mapping + +Legacy root keys, and matching legacy CLI flags where available, map as follows: + +| Legacy parameter | Nested TOML parameter | Notes | +|---|---|---| +| `lod12` | `[reconstruction] lod12` | CLI flag remains supported. | +| `lod13` | `[reconstruction] lod13` | CLI flag remains supported. | +| `lod22` | `[reconstruction] lod22` | CLI flag remains supported. | +| `clip-terrain` | `[reconstruction] clip-terrain` | CLI flag remains supported. | +| `lod13-step-height` | `[reconstruction] lod13-step-height` | Relocated. | +| `h-terrain-strategy` | `[reconstruction] h-terrain-strategy` | Relocated. | +| `complexity-factor` | `[reconstruction.arrangement-optimiser] complexity-factor` | CLI flag remains supported. | +| `plane-detect-k` | `[reconstruction.plane-detector] plane-neighbour-count` | Renamed. | +| `plane-detect-min-points` | `[reconstruction.plane-detector] min-plane-points` | Renamed. | +| `plane-detect-epsilon` | `[reconstruction.plane-detector] plane-epsilon` | Renamed. | +| `plane-detect-normal-angle` | `[reconstruction.plane-detector] plane-normal-threshold` | Renamed. | +| `line-detect-epsilon` | `[reconstruction.line-detector] distance-threshold` | Renamed. | +| `thres-alpha` | `[reconstruction.alpha-shaper] alpha` | Renamed. | +| `thres-reg-line-dist` | `[reconstruction.line-regulariser] distance-threshold` | Renamed. | +| `thres-reg-line-ext` | `[reconstruction.line-regulariser] extension` | Renamed. | +| `lod11-fallback-planes` | `[reconstruction.plane-detector] max-plane-count` | Renamed; deterministic complexity cutoff. | +| `lod11-fallback-time` | `[reconstruction.plane-detector] max-plane-count` | The old value is ignored; `max-plane-count` is the deterministic replacement. | ## [1.0.0] - 2026-04-20 diff --git a/apps/roofer-app/config.hpp b/apps/roofer-app/config.hpp index 442e615e..85b8c5b8 100644 --- a/apps/roofer-app/config.hpp +++ b/apps/roofer-app/config.hpp @@ -25,6 +25,7 @@ #include #include #include +#include #include "toml.hpp" #include "fmt/format.h" #include "fmt/ranges.h" @@ -33,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -40,15 +42,6 @@ #include #include "version.hpp" -namespace roofer::enums { - enum TerrainStrategy { - BUFFER_TILE = 0, - BUFFER_USER = 1, - USER = 2 - // POLYGON_Z = 3 - }; -} - #include "validators.hpp" #include "parameter.hpp" @@ -173,26 +166,9 @@ struct RooferConfig { std::string metadata_json_file_spec = "{path}/metadata.json"; std::string output_path; - // reconstruct - roofer::enums::TerrainStrategy h_terrain_strategy = - roofer::enums::TerrainStrategy::BUFFER_TILE; - int lod11_fallback_planes = 900; - float complexity_factor = 0.888; - - bool clip_ground = true; - bool lod_12 = false; - bool lod_13 = false; - bool lod_22 = true; - float lod13_step_height = 3.; - float floor_elevation = 0.; - int plane_detect_k = 15; - int plane_detect_min_points = 15; - float plane_detect_epsilon = 0.300000; - float plane_detect_normal_angle = 0.750000; - float line_detect_epsilon = 1.000000; - float thres_alpha = 0.250000; - float thres_reg_line_dist = 0.800000; - float thres_reg_line_ext = 3.000000; + // reconstruct: defaults and low-level options are owned by the shared + // descriptor-backed aggregate. + roofer::reconstruction::ReconstructionConfig reconstruction; // output attribute names std::string a_success = "rf_success"; @@ -321,6 +297,80 @@ struct CLIArgs { } }; +struct LegacyReconstructionDestination { + std::string_view legacy_key; + std::string_view section; + std::string_view nested_key; + bool cli_deprecated = true; + bool ignored = false; + bool cli_supported = true; +}; + +#define ROOFER_LEGACY_RECONSTRUCTION_ALIASES(X) \ + X("lod12", "reconstruction", "lod12", false, false, true, FIELD, "", \ + cfg_.reconstruction, Reconstruction, lod12) \ + X("lod13", "reconstruction", "lod13", false, false, true, FIELD, "", \ + cfg_.reconstruction, Reconstruction, lod13) \ + X("lod22", "reconstruction", "lod22", false, false, true, FIELD, "", \ + cfg_.reconstruction, Reconstruction, lod22) \ + X("complexity-factor", "reconstruction.arrangement-optimiser", \ + "complexity-factor", false, false, true, FIELD, "", \ + cfg_.reconstruction.arrangement_optimiser, Optimiser, complexity_factor) \ + X("clip-terrain", "reconstruction", "clip-terrain", false, false, true, \ + FIELD, "", cfg_.reconstruction, Reconstruction, clip_terrain) \ + X("lod13-step-height", "reconstruction", "lod13-step-height", true, false, \ + true, FIELD, "", cfg_.reconstruction, Reconstruction, lod13_step_height) \ + X("plane-detect-k", "reconstruction.plane-detector", \ + "plane-neighbour-count", true, false, true, FIELD, "", \ + cfg_.reconstruction.plane_detector, PlaneDetector, plane_neighbour_count) \ + X("plane-detect-min-points", "reconstruction.plane-detector", \ + "min-plane-points", true, false, true, FIELD, "", \ + cfg_.reconstruction.plane_detector, PlaneDetector, min_plane_points) \ + X("plane-detect-epsilon", "reconstruction.plane-detector", "plane-epsilon", \ + true, false, true, FIELD, "", cfg_.reconstruction.plane_detector, \ + PlaneDetector, plane_epsilon) \ + X("plane-detect-normal-angle", "reconstruction.plane-detector", \ + "plane-normal-threshold", true, false, false, FIELD, "", \ + cfg_.reconstruction.plane_detector, PlaneDetector, plane_normal_threshold) \ + X("line-detect-epsilon", "reconstruction.line-detector", \ + "distance-threshold", true, false, false, FIELD, "", \ + cfg_.reconstruction.line_detector, LineDetector, distance_threshold) \ + X("thres-alpha", "reconstruction.alpha-shaper", "alpha", true, false, false, \ + FIELD, "", cfg_.reconstruction.alpha_shaper, AlphaShaper, alpha) \ + X("thres-reg-line-dist", "reconstruction.line-regulariser", \ + "distance-threshold", true, false, false, FIELD, "", \ + cfg_.reconstruction.line_regulariser, LineRegulariser, distance_threshold) \ + X("thres-reg-line-ext", "reconstruction.line-regulariser", "extension", \ + true, false, false, FIELD, "", cfg_.reconstruction.line_regulariser, \ + LineRegulariser, extension) \ + X("h-terrain-strategy", "reconstruction", "h-terrain-strategy", true, false, \ + true, FIELD, "\"buffer_tile\"", cfg_.reconstruction, Reconstruction, \ + h_terrain_strategy) \ + X("lod11-fallback-planes", "reconstruction.plane-detector", \ + "max-plane-count", true, false, true, FIELD, "", \ + cfg_.reconstruction.plane_detector, PlaneDetector, max_plane_count) \ + X("lod11-fallback-time", "reconstruction.plane-detector", "max-plane-count", \ + true, true, true, IGNORED, "", _deprecated_lod11_fallback_time, void, \ + unused) + +inline std::optional +legacy_reconstruction_destination(std::string_view key) { +#define ROOFER_LEGACY_DESTINATION(key, section, nested_key, deprecated, \ + ignored, cli_supported, kind, example, \ + owner, type, member) \ + LegacyReconstructionDestination{key, section, nested_key, \ + deprecated, ignored, cli_supported}, + static constexpr std::array destinations{ + ROOFER_LEGACY_RECONSTRUCTION_ALIASES(ROOFER_LEGACY_DESTINATION)}; +#undef ROOFER_LEGACY_DESTINATION + + const auto destination = + std::find_if(destinations.begin(), destinations.end(), + [key](const auto& item) { return item.legacy_key == key; }); + if (destination == destinations.end()) return std::nullopt; + return *destination; +} + struct RooferConfigHandler { RooferConfig cfg_; @@ -332,6 +382,9 @@ struct RooferConfigHandler { DocAttribMap output_attr_; std::unordered_map param_index_; std::unordered_map app_param_index_; + ParameterVector root_only_reconstruction_aliases_; + std::unordered_map + root_only_reconstruction_alias_index_; static int default_jobs() { auto system_threads = std::thread::hardware_concurrency(); @@ -350,6 +403,7 @@ struct RooferConfigHandler { int _trace_interval = 10; std::string _config_path; int _jobs = default_jobs(); + int _deprecated_lod11_fallback_time = 1800000; // methods RooferConfigHandler() { @@ -363,11 +417,11 @@ struct RooferConfigHandler { general.add("jobs", 'j', "Number of worker jobs to use. Reconstruction uses roughly " "jobs - 1 threads.", - _jobs, {check::HigherThan(0)}); + _jobs, {roofer::config::greater_than(0)}); general.add("config", 'c', "Configuration file", _config_path, {check::PathExists, check::DirIsWritable}); general.add("trace-interval", "Interval for tracing in seconds", - _trace_interval, {check::HigherThan(0)}); + _trace_interval, {roofer::config::greater_than(0)}); general.add("loglevel", "Specify loglevel", _loglevel); #ifdef RF_USE_RERUN general.add("rerun", "Log intermediate results to rerun", cfg_.use_rerun); @@ -434,10 +488,10 @@ struct RooferConfigHandler { // selection input.add("bld-class", "LAS classification code that contains the building points.", - cfg_.bld_class, {check::HigherOrEqualTo(0)}); + cfg_.bld_class, {roofer::config::at_least(0)}); input.add("grnd-class", "LAS classification code that constains the ground points.", - cfg_.grnd_class, {check::HigherOrEqualTo(0)}); + cfg_.grnd_class, {roofer::config::at_least(0)}); input.add("skip-pc-check", "Disable/enable check if all supplied pointcloud files exist.", _skip_pc_check); @@ -448,27 +502,27 @@ struct RooferConfigHandler { cfg_.simplify); crop.add("ceil-point-density", "Enforce this point density ceiling on each building pointcloud.", - cfg_.ceil_point_density, {check::HigherThan(0)}); + cfg_.ceil_point_density, {roofer::config::greater_than(0.0F)}); crop.add("cellsize", "Cellsize used for quick pointcloud analysis (eg. point density" " and nodata regions).", - cfg_.cellsize, {check::HigherThan(0)}); + cfg_.cellsize, {roofer::config::greater_than(0.0F)}); crop.add("min-building-density", "Minimum building-class point density (points/m²) below " "which a rootprint's pointcloud is flagged insufficient.", - cfg_.min_building_density, {check::HigherOrEqualTo(0)}); + cfg_.min_building_density, {roofer::config::at_least(0.0F)}); crop.add("max-nodata-fraction", "Maximum fraction of the roofprint area without pointcloud data. " "Above this threshold, a rootprint's pointcloud is flagged " "insufficient.", - cfg_.max_nodata_fraction, {check::InRange(0, 1)}); + cfg_.max_nodata_fraction, {roofer::config::in_range(0.0F, 1.0F)}); crop.add("terrain-grid-cellsize", "Cellsize used for the crop phase terrain fallback grid.", - cfg_.terrain_grid_cellsize, {check::HigherThan(0)}); + cfg_.terrain_grid_cellsize, {roofer::config::greater_than(0.0F)}); crop.add("terrain-grid-search-radius", "Number of terrain grid cells to search around a building when " "its local fallback cells do not contain terrain points.", - cfg_.terrain_grid_search_radius, {check::HigherOrEqualTo(0)}); + cfg_.terrain_grid_search_radius, {roofer::config::at_least(0)}); crop.add("terrain-nodata-mode", "How missing terrain grid samples are handled during output " "triangulation: `complete_quads`, `local_triangles`, or " @@ -481,7 +535,7 @@ struct RooferConfigHandler { "LoD 1.1 fallback threshold area in square meters. If the area of the " "roofprint is larger than this value, the building will be always be " "reconstructed using a LoD 1.1 extrusion.", - cfg_.lod11_fallback_area, {check::HigherThan(0)}); + cfg_.lod11_fallback_area, {roofer::config::greater_than(0)}); crop.add("clear-insufficient", "Do not attempt to reconstruct buildings with insufficient " "pointcloud data." @@ -514,80 +568,48 @@ struct RooferConfigHandler { "crop phase.", cfg_.write_index); - reconstruction.add("lod12", - "Generate LoD 1.2 geometries in CityJSONSeq output.", - cfg_.lod_12); - reconstruction.add("lod13", - "Generate LoD 1.3 geometries in CityJSONSeq output.", - cfg_.lod_13); - reconstruction.add("lod22", - "Generate LoD 2.2 geometries in CityJSONSeq output.", - cfg_.lod_22); - reconstruction.add( - "complexity-factor", - "Complexity factor for building model geometry. " - "A number between 0.0 and 1.0. Higher values lead to more detailed " - "building models, lower values to simpler models.", - cfg_.complexity_factor, {check::InRange(0, 1)}); - reconstruction.add( - "clip-terrain", - "Set to true to activate the procedure that clips parts from the " - "input roofprint wherever patches of ground points are detected." - "May cause irregular outlines in reconstruction result.", - cfg_.clip_ground, {}); - reconstruction.add( - "lod13-step-height", - "Step height in meters, used for LoD 1.3 generalisation." - "Adjacent roofparts with a height discontinuity that is smaller " - "than this value are merged. Only affects LoD 1.3 geometry.", - cfg_.lod13_step_height, {check::HigherThan(0)}); - reconstruction.add("plane-detect-k", - "Number of points used in nearest neighbour queries for " - "plane detection. Higher values will lead to longer " - "processing times, but may help with growing plane " - "regions through areas with a poor point distribution.", - cfg_.plane_detect_k, {check::HigherThan(0)}); - reconstruction.add("plane-detect-min-points", - "Minimum number of points required for " - "detecting a plane in the pointcloud.", - cfg_.plane_detect_min_points, - {check::HigherThan(2)}); - reconstruction.add("plane-detect-epsilon", - "Maximum distance (in meters) from inliers to" - " plane during plane fitting procedure. Higher values " - "offer more robustness " - "against oversegmentation, but may result in less " - "accurate plane detection.", - cfg_.plane_detect_epsilon, - {check::HigherThan(0)}); - reconstruction - .add("h-terrain-strategy", - "Strategy to determine terrain elevation " - "that is used to set the height of building floors. " - "`buffer_tile`: use the 5th percentile lowest elevation point in " - "a 4 meter buffer around the roofprint. If no points are found, " - "we fall back to the local terrain grid minimum. If no local " - "terrain grid cells are found, we fall back to the lowest " - "elevation point in the current tile. " - "This may give undesired results for hilly areas. " - "`buffer_user`: use the same buffered pointcloud value, then " - "fall back to `--h-terrain-attribute`, then the local terrain " - "grid minimum, then the tile minimum. `user`: use " - "`--h-terrain-attribute`, then the local terrain grid minimum, " - "then the tile minimum.", - cfg_.h_terrain_strategy) - .example_ = "\"buffer_tile\""; - reconstruction.add( - "lod11-fallback-planes", - "Number of planes required for LoD 1.1 fallback. When more than this " - "number of planes is detected, abort the reconstruction process and " - "fallback to LoD 1.1 extrusion. This plane count is a deterministic " - "complexity cutoff that bounds reconstruction time per building.", - cfg_.lod11_fallback_planes, {check::HigherThan(0)}); + using Reconstruction = roofer::reconstruction::ReconstructionConfig; + using Optimiser = roofer::reconstruction::ArrangementOptimiserConfig; + using PlaneDetector = roofer::reconstruction::PlaneDetectorConfig; + using AlphaShaper = roofer::reconstruction::AlphaShaperConfig; + using LineDetector = roofer::reconstruction::LineDetectorConfig; + using LineRegulariser = roofer::reconstruction::LineRegulariserConfig; + +#define ROOFER_REGISTER_LEGACY_FIELD(key, cli_supported, example, owner, type, \ + member) \ + do { \ + auto& parameter = \ + (cli_supported) \ + ? reconstruction.add_from_field(key, owner, &type::member) \ + : root_only_reconstruction_aliases_.add_from_field(key, owner, \ + &type::member); \ + parameter.example_ = example; \ + } while (false); +#define ROOFER_REGISTER_LEGACY_IGNORED(key, cli_supported, example, owner, \ + type, member) \ + do { \ + static_assert(cli_supported); \ + auto& parameter = reconstruction.add( \ + key, \ + "Deprecated and ignored. Wall-clock reconstruction limits were " \ + "removed because they are nondeterministic; use max-plane-count " \ + "under [reconstruction.plane-detector] instead.", \ + owner); \ + parameter.example_ = example; \ + } while (false); +#define ROOFER_REGISTER_LEGACY(key, section, nested_key, deprecated, ignored, \ + cli_supported, kind, example, owner, type, \ + member) \ + ROOFER_REGISTER_LEGACY_##kind(key, cli_supported, example, owner, type, \ + member) + ROOFER_LEGACY_RECONSTRUCTION_ALIASES(ROOFER_REGISTER_LEGACY) +#undef ROOFER_REGISTER_LEGACY +#undef ROOFER_REGISTER_LEGACY_IGNORED +#undef ROOFER_REGISTER_LEGACY_FIELD output.add("tiling", "Enable or disable output tiling.", _tiling); output.add("tilesize", "Tilesize for rectangular output tiles in meters.", - cfg_.tilesize, {check::HigherThan({0, 0})}); + cfg_.tilesize, {check::AllHigherThan({0, 0})}); output.add("split-cjseq", "Output CityJSONSequence file for each building instead of one " "file per tile.", @@ -769,6 +791,8 @@ struct RooferConfigHandler { for (auto& [group_name, group] : app_param_groups_) { group.add_to_index(app_param_index_); } + root_only_reconstruction_aliases_.add_to_index( + root_only_reconstruction_alias_index_); }; void validate() { @@ -787,6 +811,9 @@ struct RooferConfigHandler { } } } + if (auto error = cfg_.reconstruction.validate()) { + throw std::runtime_error("Validation error for reconstruction." + *error); + } if (input_pointclouds_.empty()) { throw std::runtime_error("No input pointclouds specified."); @@ -822,6 +849,69 @@ struct RooferConfigHandler { } } + template + bool assign_reconstruction_field(const toml::key& key, const toml::node& node, + const std::string& parent_path, + Config& config) { + bool found = false; + roofer::config::for_each_field(config, [&](auto field, auto& field_value) { + if (found || key != field.toml_name()) return; + found = true; + const auto path = parent_path + "." + field.toml_name(); + if (!assign_toml_value(node, field_value)) { + throw std::runtime_error("Invalid value type or value for " + path + + "."); + } + if (field.validator) { + if (auto error = field.validator(field_value)) { + throw std::runtime_error("Validation error for " + path + ": " + + *error + "."); + } + } + }); + return found; + } + + template + void parse_reconstruction_component(const toml::table& table, + const std::string& path, + Component& component) { + for (const auto& [key, node] : table) { + if (!assign_reconstruction_field(key, node, path, component)) { + throw std::runtime_error("Unknown reconstruction parameter: " + path + + "." + std::string(key.str()) + "."); + } + } + } + + void parse_reconstruction_table(const toml::table& table) { + for (const auto& [key, node] : table) { + if (assign_reconstruction_field(key, node, "reconstruction", + cfg_.reconstruction)) { + continue; + } + + bool found = false; + cfg_.reconstruction.visit_components( + [&](std::string_view component_name, auto& component) { + if (!found && key == component_name) { + found = true; + const auto* component_table = node.as_table(); + const auto path = "reconstruction." + std::string(component_name); + if (!component_table) { + throw std::runtime_error(path + " must be a table."); + } + parse_reconstruction_component(*component_table, path, component); + } + }); + if (!found) { + throw std::runtime_error( + "Unknown reconstruction parameter: reconstruction." + + std::string(key.str()) + "."); + } + } + } + void print_help_header(const std::string& program_name, bool compact) { // see http://docopt.org/ std::cout << "Automatic LoD 2.2 building reconstruction from " @@ -1090,6 +1180,23 @@ struct RooferConfigHandler { } void parse_cli_second_pass(CLIArgs& c) { + auto& logger = roofer::logger::Logger::get_logger(); + const auto warn_if_legacy = [&](const std::string& name) { + if (const auto destination = legacy_reconstruction_destination(name); + destination && destination->cli_deprecated) { + if (destination->ignored) { + logger.warning( + "Option --{} is deprecated and ignored; use {} under [{}] in " + "the configuration file instead. It will be rejected in 2.0.", + name, destination->nested_key, destination->section); + } else { + logger.warning( + "Option --{} is deprecated; use {} under [{}] in the " + "configuration file. It will be removed in 2.0.", + name, destination->nested_key, destination->section); + } + } + }; auto it = c.args.begin(); while (it != c.args.end()) { std::string arg = *it; @@ -1098,6 +1205,7 @@ struct RooferConfigHandler { if (arg.starts_with("--no")) { auto argname = arg.substr(5); if (auto p = param_index_.find(argname); p != param_index_.end()) { + warn_if_legacy(argname); it = c.args.erase(it); p->second->unset(); } else { @@ -1106,6 +1214,7 @@ struct RooferConfigHandler { } else if (arg.starts_with("--")) { auto argname = arg.substr(2); if (auto p = param_index_.find(argname); p != param_index_.end()) { + warn_if_legacy(argname); it = c.args.erase(it); it = p->second->set(c.args, it); } else { @@ -1171,6 +1280,20 @@ struct RooferConfigHandler { // iterate config table for (const auto& [key, value] : config) { try { + if (const auto destination = + legacy_reconstruction_destination(key.str())) { + if (destination->ignored) { + logger.warning( + "Root configuration key {} is deprecated and ignored; use {} " + "under [{}] instead. It will be rejected in 2.0.", + key.data(), destination->nested_key, destination->section); + } else { + logger.warning( + "Root configuration key {} is deprecated; use {} under [{}] " + "instead. It will be removed in 2.0.", + key.data(), destination->nested_key, destination->section); + } + } if (key == "polygon-source") { get_toml_value(config, "polygon-source", cfg_.source_footprints); } else if (key == "output-directory") { @@ -1191,6 +1314,13 @@ struct RooferConfigHandler { } } } + } else if (key == "reconstruction") { + // Parsed after all root-level compatibility aliases so nested values + // deterministically take precedence, independent of table ordering. + } else if (auto p = + root_only_reconstruction_alias_index_.find(key.data()); + p != root_only_reconstruction_alias_index_.end()) { + p->second->set_from_toml(config, key.data()); } else if (auto p = param_index_.find(key.data()); p != param_index_.end()) { p->second->set_from_toml(config, key.data()); @@ -1252,9 +1382,16 @@ struct RooferConfigHandler { key.data(), e.what())); } } + if (const auto* reconstruction = config["reconstruction"].as_table()) { + parse_reconstruction_table(*reconstruction); + } else if (config.contains("reconstruction")) { + throw std::runtime_error("reconstruction must be a table."); + } } }; +#undef ROOFER_LEGACY_RECONSTRUCTION_ALIASES + template <> struct fmt::formatter { static constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); } diff --git a/apps/roofer-app/crop_tile.hpp b/apps/roofer-app/crop_tile.hpp index ede7e745..319b05ec 100644 --- a/apps/roofer-app/crop_tile.hpp +++ b/apps/roofer-app/crop_tile.hpp @@ -431,7 +431,8 @@ bool crop_tile(const roofer::TBox& tile, input_pointclouds[selected->index].ground_elevations[i]; auto h_ground_terrain_grid = input_pointclouds[selected->index].terrain_grid_elevations[i]; - if (cfg.h_terrain_strategy == TerrainStrategy::BUFFER_TILE) { + if (cfg.reconstruction.h_terrain_strategy == + TerrainStrategy::BUFFER_TILE) { if (h_ground_pc.has_value()) { building.h_ground = h_ground_pc.value(); } else if (h_ground_terrain_grid.has_value()) { @@ -440,7 +441,8 @@ bool crop_tile(const roofer::TBox& tile, building.h_ground = input_pointclouds[selected->index].min_ground_elevation; } - } else if (cfg.h_terrain_strategy == TerrainStrategy::BUFFER_USER) { + } else if (cfg.reconstruction.h_terrain_strategy == + TerrainStrategy::BUFFER_USER) { if (h_ground_pc.has_value()) { building.h_ground = h_ground_pc.value(); } else { @@ -462,7 +464,8 @@ bool crop_tile(const roofer::TBox& tile, exit(1); } } - } else if (cfg.h_terrain_strategy == TerrainStrategy::USER) { + } else if (cfg.reconstruction.h_terrain_strategy == + TerrainStrategy::USER) { if (h_ground_fallback_vec) { if ((*h_ground_fallback_vec)[i].has_value()) { building.h_ground = (*h_ground_fallback_vec)[i].value(); diff --git a/apps/roofer-app/doc-helper.cpp b/apps/roofer-app/doc-helper.cpp index 9870186d..17915cc8 100644 --- a/apps/roofer-app/doc-helper.cpp +++ b/apps/roofer-app/doc-helper.cpp @@ -22,6 +22,42 @@ #include #include "config.hpp" +template +std::string reconstruction_toml_value(const T& value) { + if constexpr (std::is_same_v) { + return value ? "true" : "false"; + } else if constexpr (std::is_same_v) { + return '"' + std::format("{}", value) + '"'; + } else if constexpr (std::is_same_v< + T, roofer::reconstruction::PointCountRange>) { + return std::format("[{}, {}]", value.first, value.second); + } else { + return std::format("{}", value); + } +} + +template +void print_reconstruction_fields(const Config& config) { + roofer::config::for_each_field(config, [](auto field, const auto& value) { + std::cout << "## " << field.description << "\n"; + std::cout << field.toml_name() << " = " << reconstruction_toml_value(value) + << "\n"; + }); +} + +void print_reconstruction_toml( + const roofer::reconstruction::ReconstructionConfig& config) { + std::cout << "### Reconstruction options\n[reconstruction]\n"; + print_reconstruction_fields(config); + config.visit_components([&](std::string_view name, const auto& component) { + using Component = std::remove_cvref_t; + if (!roofer::config::has_public_fields()) return; + std::cout << "\n[reconstruction." << name << "]\n"; + print_reconstruction_fields(component); + }); + std::cout << "\n"; +} + void print_params(RooferConfigHandler::param_group_map& params) { for (const auto& [group_name, param_list] : params) { std::cout << "### " << group_name << " options\n\n"; @@ -34,7 +70,9 @@ void print_params(RooferConfigHandler::param_group_map& params) { } } -void print_params_as_toml(RooferConfigHandler::param_group_map& params) { +void print_params_as_toml( + RooferConfigHandler::param_group_map& params, + const roofer::reconstruction::ReconstructionConfig& reconstruction) { std::cout << "## Path to roofprint polygons source. Can be an OGR supported " "file (eg. GPKG) or database connection string.\n"; std::cout << "# polygon-source = \"\"\n"; @@ -42,6 +80,7 @@ void print_params_as_toml(RooferConfigHandler::param_group_map& params) { "CityJSONSequence file in this directory.\n"; std::cout << "# output-directory = \"\"\n\n"; for (const auto& [group_name, param_list] : params) { + if (group_name == "Reconstruction") continue; std::cout << std::format("### {} options\n", group_name); for (const auto& param : param_list) { std::cout << std::format("## {}\n", param->description()); @@ -67,6 +106,7 @@ void print_params_as_toml(RooferConfigHandler::param_group_map& params) { } std::cout << "\n"; } + print_reconstruction_toml(reconstruction); // print pointcloud block InputPointcloud pc_defaults{}; std::cout << "[[pointclouds]]\n"; @@ -109,7 +149,7 @@ int main(int argc, const char* argv[]) { RooferConfigHandler rch{}; if (format == "config") { - print_params_as_toml(rch.param_groups_); + print_params_as_toml(rch.param_groups_, rch.cfg_.reconstruction); } else if (format == "attr") { print_attributes(rch.output_attr_); } else if (format == "params") { diff --git a/apps/roofer-app/parameter.hpp b/apps/roofer-app/parameter.hpp index 4672e30f..4f465e2b 100644 --- a/apps/roofer-app/parameter.hpp +++ b/apps/roofer-app/parameter.hpp @@ -20,6 +20,7 @@ // Ravi Peters // Balázs Dukai #pragma once +#include #include #include #include @@ -109,6 +110,95 @@ struct std::formatter { } }; +/** Decode the scalar-like TOML types shared by app and reconstruction config. + */ +template +bool assign_toml_value(const toml::node& node, T& result) { + if constexpr (std::is_same_v) { + if (auto value = node.value()) { + result = static_cast(*value); + return true; + } + if (auto value = node.value()) { + result = static_cast(*value); + return true; + } + } else if constexpr (std::is_same_v) { + if (auto value = node.value()) { + result = *value; + return true; + } + if (auto value = node.value()) { + result = static_cast(*value); + return true; + } + } else if constexpr (std::is_same_v) { + if (auto value = node.value()) { + result = *value; + return true; + } + } else if constexpr (std::is_same_v) { + if (auto value = node.value()) { + result = *value; + return true; + } + } else if constexpr (std::is_same_v) { + if (auto value = node.value()) { + result = std::move(*value); + return true; + } + } else if constexpr (std::is_same_v>) { + int value; + if (assign_toml_value(node, value)) { + result = value; + return true; + } + } else if constexpr (std::is_same_v>) { + float value; + if (assign_toml_value(node, value)) { + result = value; + return true; + } + } else if constexpr (std::is_same_v>) { + double value; + if (assign_toml_value(node, value)) { + result = value; + return true; + } + } else if constexpr (std::is_same_v>) { + std::string value; + if (assign_toml_value(node, value)) { + result = std::move(value); + return true; + } + } else if constexpr (std::is_same_v>) { + if (const auto* values = node.as_array(); values && values->size() == 2) { + auto lower = (*values)[0].value(); + auto upper = (*values)[1].value(); + if (lower && upper) { + result = {*lower, *upper}; + return true; + } + } + } else if constexpr (std::is_same_v) { + if (auto value = node.value()) { + if (*value == "buffer_tile") { + result = roofer::enums::TerrainStrategy::BUFFER_TILE; + return true; + } + if (*value == "buffer_user") { + result = roofer::enums::TerrainStrategy::BUFFER_USER; + return true; + } + if (*value == "user") { + result = roofer::enums::TerrainStrategy::USER; + return true; + } + } + } + return false; +} + struct ConfigParameter { std::string help_; std::string longname_; @@ -149,17 +239,18 @@ template struct ConfigParameterByReference : public ConfigParameter { T& value_; T default_value_; - std::vector> _validators; + std::vector> _validators; - ConfigParameterByReference(std::string longname, std::string help, T& value, - std::vector> validators) + ConfigParameterByReference( + std::string longname, std::string help, T& value, + std::vector> validators) : ConfigParameter(longname, help), value_(value), default_value_(value), _validators(validators){}; - ConfigParameterByReference(std::string longname, char shortname, - std::string help, T& value, - std::vector> validators) + ConfigParameterByReference( + std::string longname, char shortname, std::string help, T& value, + std::vector> validators) : ConfigParameter(longname, shortname, help), value_(value), default_value_(value), @@ -375,17 +466,10 @@ struct ConfigParameterByReference : public ConfigParameter { } } } else if constexpr (std::is_same_v) { - if (const toml::value* s = table[name].as_string()) { - if (*s == "buffer_tile") { - value_ = roofer::enums::TerrainStrategy::BUFFER_TILE; - } else if (*s == "buffer_user") { - value_ = roofer::enums::TerrainStrategy::BUFFER_USER; - } else if (*s == "user") { - value_ = roofer::enums::TerrainStrategy::USER; - } else { - throw std::runtime_error("Failed to read value for " + name + - " from config file."); - } + const auto* node = table.get(name); + if (!node || !assign_toml_value(*node, value_)) { + throw std::runtime_error("Failed to read value for " + name + + " from config file."); } } else if constexpr (std::is_same_v) { if (const toml::value* s = table[name].as_string()) { @@ -413,9 +497,8 @@ struct ConfigParameterByReference : public ConfigParameter { } } } else { - if (auto value = table[name].value(); value.has_value()) { - value_ = *value; - } else { + const auto* node = table.get(name); + if (!node || !assign_toml_value(*node, value_)) { std::string expected; if constexpr (std::is_same_v) { expected = "boolean"; @@ -492,22 +575,35 @@ class ParameterVector { ParameterVector& operator=(const ParameterVector&) = delete; template - ConfigParameter& add(const std::string& longname, const std::string& help, - T& value, std::vector> validators = {}) { + ConfigParameter& add( + const std::string& longname, const std::string& help, T& value, + std::vector> validators = {}) { params_.emplace_back(std::make_unique>( longname, help, value, std::move(validators))); return *params_.back(); } template - ConfigParameter& add(const std::string& longname, const char shortname, - const std::string& help, T& value, - std::vector> validators = {}) { + ConfigParameter& add( + const std::string& longname, const char shortname, + const std::string& help, T& value, + std::vector> validators = {}) { params_.emplace_back(std::make_unique>( longname, shortname, help, value, std::move(validators))); return *params_.back(); } + /** Register a CLI/TOML alias while reusing canonical field metadata. */ + template + ConfigParameter& add_from_field(const std::string& longname, Owner& owner, + T Owner::*member) { + const auto field = roofer::config::field_for(member); + std::vector> validators; + if (field.validator) validators.push_back(field.validator); + return add(longname, std::string(field.description), owner.*member, + std::move(validators)); + } + void add_to_index(std::unordered_map& index) { for (auto& param : params_) { index[param->longname_] = param.get(); diff --git a/apps/roofer-app/reconstruct_building.hpp b/apps/roofer-app/reconstruct_building.hpp index d9c86606..a7881b77 100644 --- a/apps/roofer-app/reconstruct_building.hpp +++ b/apps/roofer-app/reconstruct_building.hpp @@ -206,11 +206,13 @@ std::unordered_map extrude_lod22( auto ArrangementDissolver = roofer::reconstruction::createArrangementDissolver(); - ArrangementDissolver->compute( - arrangement, SegmentRasteriser->heightfield, *elevation_provider, - {.dissolve_step_edges = dissolve_step_edges, - .dissolve_all_interior = dissolve_all_interior, - .step_height_threshold = cfg->lod13_step_height}); + auto dissolver_config = cfg->reconstruction.arrangement_dissolver; + dissolver_config.dissolve_step_edges = dissolve_step_edges; + dissolver_config.dissolve_all_interior = dissolve_all_interior; + dissolver_config.step_height_threshold = + cfg->reconstruction.lod13_step_height; + ArrangementDissolver->compute(arrangement, SegmentRasteriser->heightfield, + *elevation_provider, dissolver_config); // logger.debug("Completed ArrangementDissolver"); // logger.debug("Roof partition has {} faces", arrangement.number_of_faces()); #ifdef RF_USE_RERUN @@ -221,7 +223,8 @@ std::unordered_map extrude_lod22( } #endif auto ArrangementSnapper = roofer::reconstruction::createArrangementSnapper(); - ArrangementSnapper->compute(arrangement, *elevation_provider); + ArrangementSnapper->compute(arrangement, *elevation_provider, + cfg->reconstruction.arrangement_snapper); // logger.debug("Completed ArrangementSnapper"); #ifdef RF_USE_RERUN // rec.log(worldname+"ArrangementSnapper", rerun::LineStrips3D( @@ -230,8 +233,10 @@ std::unordered_map extrude_lod22( auto ArrangementExtruder = roofer::reconstruction::createArrangementExtruder(); + auto extruder_config = cfg->reconstruction.arrangement_extruder; + extruder_config.lod2 = extrude_LoD2; ArrangementExtruder->compute(arrangement, *elevation_provider, - {.LoD2 = extrude_LoD2}); + extruder_config); // logger.debug("Completed ArrangementExtruder"); #ifdef RF_USE_RERUN if (cfg->use_rerun) { @@ -335,14 +340,7 @@ void reconstruct_building(BuildingObject& building, RooferConfig* cfg) { auto PlaneDetector = roofer::reconstruction::createPlaneDetector(); auto PlaneDetector_ground = roofer::reconstruction::createPlaneDetector(); try { - auto plane_detector_cfg = roofer::reconstruction::PlaneDetectorConfig{ - .metrics_plane_k = cfg->plane_detect_k, - .metrics_plane_min_points = cfg->plane_detect_min_points, - .metrics_plane_epsilon = cfg->plane_detect_epsilon, - .metrics_plane_normal_threshold = cfg->plane_detect_normal_angle, - .with_limits = true, - .limit_n_regions = cfg->lod11_fallback_planes, - }; + auto plane_detector_cfg = cfg->reconstruction.plane_detector; PlaneDetector->detect(building.pointcloud_building, plane_detector_cfg); timings["PlaneDetector"] = std::chrono::high_resolution_clock::now() - t0; t0 = std::chrono::high_resolution_clock::now(); @@ -395,7 +393,7 @@ void reconstruct_building(BuildingObject& building, RooferConfig* cfg) { t0 = std::chrono::high_resolution_clock::now(); auto AlphaShaper = roofer::reconstruction::createAlphaShaper(); AlphaShaper->compute(PlaneDetector->pts_per_roofplane, - {.thres_alpha = cfg->thres_alpha}); + cfg->reconstruction.alpha_shaper); timings["AlphaShaper"] = std::chrono::high_resolution_clock::now() - t0; // logger.debug("Completed AlphaShaper (roof), found {} rings, {} labels", // AlphaShaper->alpha_rings.size(), @@ -409,7 +407,8 @@ void reconstruct_building(BuildingObject& building, RooferConfig* cfg) { #endif t0 = std::chrono::high_resolution_clock::now(); auto AlphaShaper_ground = roofer::reconstruction::createAlphaShaper(); - AlphaShaper_ground->compute(PlaneDetector_ground->pts_per_roofplane); + AlphaShaper_ground->compute(PlaneDetector_ground->pts_per_roofplane, + cfg->reconstruction.alpha_shaper); timings["AlphaShaper_ground"] = std::chrono::high_resolution_clock::now() - t0; // logger.debug("Completed AlphaShaper (ground), found {} rings, {} labels", @@ -426,7 +425,7 @@ void reconstruct_building(BuildingObject& building, RooferConfig* cfg) { auto LineDetector = roofer::reconstruction::createLineDetector(); LineDetector->detect(AlphaShaper->alpha_rings, AlphaShaper->roofplane_ids, PlaneDetector->pts_per_roofplane, - {.dist_thres = cfg->line_detect_epsilon}); + cfg->reconstruction.line_detector); timings["LineDetector"] = std::chrono::high_resolution_clock::now() - t0; // logger.debug("Completed LineDetector"); #ifdef RF_USE_RERUN @@ -439,7 +438,8 @@ void reconstruct_building(BuildingObject& building, RooferConfig* cfg) { t0 = std::chrono::high_resolution_clock::now(); auto PlaneIntersector = roofer::reconstruction::createPlaneIntersector(); PlaneIntersector->compute(PlaneDetector->pts_per_roofplane, - PlaneDetector->plane_adjacencies); + PlaneDetector->plane_adjacencies, + cfg->reconstruction.plane_intersector); timings["PlaneIntersector"] = std::chrono::high_resolution_clock::now() - t0; @@ -462,8 +462,7 @@ void reconstruct_building(BuildingObject& building, RooferConfig* cfg) { auto LineRegulariser = roofer::reconstruction::createLineRegulariser(); LineRegulariser->compute(LineDetector->edge_segments, PlaneIntersector->segments, - {.dist_threshold = cfg->thres_reg_line_dist, - .extension = cfg->thres_reg_line_ext}); + cfg->reconstruction.line_regulariser); timings["LineRegulariser"] = std::chrono::high_resolution_clock::now() - t0; // logger.debug("Completed LineRegulariser"); #ifdef RF_USE_RERUN @@ -475,10 +474,12 @@ void reconstruct_building(BuildingObject& building, RooferConfig* cfg) { t0 = std::chrono::high_resolution_clock::now(); auto SegmentRasteriser = roofer::reconstruction::createSegmentRasteriser(); - SegmentRasteriser->compute( - AlphaShaper->alpha_triangles, AlphaShaper_ground->alpha_triangles, - {.use_ground = - !building.pointcloud_ground.empty() && cfg->clip_ground}); + auto rasteriser_config = cfg->reconstruction.segment_rasteriser; + rasteriser_config.use_ground = + !building.pointcloud_ground.empty() && cfg->reconstruction.clip_terrain; + SegmentRasteriser->compute(AlphaShaper->alpha_triangles, + AlphaShaper_ground->alpha_triangles, + rasteriser_config); timings["SegmentRasteriser"] = std::chrono::high_resolution_clock::now() - t0; // logger.debug("Completed SegmentRasteriser"); @@ -500,7 +501,8 @@ void reconstruct_building(BuildingObject& building, RooferConfig* cfg) { auto ArrangementBuilder = roofer::reconstruction::createArrangementBuilder(); ArrangementBuilder->compute(arrangement, building.footprint, - LineRegulariser->exact_regularised_edges); + LineRegulariser->exact_regularised_edges, + cfg->reconstruction.arrangement_builder); timings["ArrangementBuilder"] = std::chrono::high_resolution_clock::now() - t0; // logger.debug("Completed ArrangementBuilder"); @@ -517,16 +519,13 @@ void reconstruct_building(BuildingObject& building, RooferConfig* cfg) { t0 = std::chrono::high_resolution_clock::now(); auto ArrangementOptimiser = roofer::reconstruction::createArrangementOptimiser(); - ArrangementOptimiser->compute( - arrangement, SegmentRasteriser->heightfield, - PlaneDetector->pts_per_roofplane, - PlaneDetector_ground->pts_per_roofplane, - { - .data_multiplier = cfg->complexity_factor, - .smoothness_multiplier = float(1. - cfg->complexity_factor), - .use_ground = - !building.pointcloud_ground.empty() && cfg->clip_ground, - }); + auto optimiser_config = cfg->reconstruction.arrangement_optimiser; + optimiser_config.use_ground = + !building.pointcloud_ground.empty() && cfg->reconstruction.clip_terrain; + ArrangementOptimiser->compute(arrangement, SegmentRasteriser->heightfield, + PlaneDetector->pts_per_roofplane, + PlaneDetector_ground->pts_per_roofplane, + optimiser_config); timings["ArrangementOptimiser"] = std::chrono::high_resolution_clock::now() - t0; // logger.debug("Completed ArrangementOptimiser"); @@ -537,19 +536,19 @@ void reconstruct_building(BuildingObject& building, RooferConfig* cfg) { // attributes to be filled during reconstruction // logger.debug("LoD={}", cfg->lod); t0 = std::chrono::high_resolution_clock::now(); - if (cfg->lod_12) { + if (cfg->reconstruction.lod12) { building.multisolids_lod12 = extrude_lod22( arrangement, building, cfg, SegmentRasteriser.get(), LOD12, building.rmse_lod12, building.volume_lod12, building.val3dity_lod12); } - if (cfg->lod_13) { + if (cfg->reconstruction.lod13) { building.multisolids_lod13 = extrude_lod22( arrangement, building, cfg, SegmentRasteriser.get(), LOD13, building.rmse_lod13, building.volume_lod13, building.val3dity_lod13); } - if (cfg->lod_22) { + if (cfg->reconstruction.lod22) { building.multisolids_lod22 = extrude_lod22( arrangement, building, cfg, SegmentRasteriser.get(), LOD22, building.rmse_lod22, building.volume_lod22, building.val3dity_lod22); diff --git a/apps/roofer-app/roofer-app.cpp b/apps/roofer-app/roofer-app.cpp index 75c6b7a9..cc173525 100644 --- a/apps/roofer-app/roofer-app.cpp +++ b/apps/roofer-app/roofer-app.cpp @@ -1032,7 +1032,7 @@ int main(int argc, const char* argv[]) { std::unordered_map* ms12 = nullptr; std::unordered_map* ms13 = nullptr; std::unordered_map* ms22 = nullptr; - if (handler.cfg_.lod_12) { + if (handler.cfg_.reconstruction.lod12) { ms12 = &building.multisolids_lod12; if (!handler.cfg_.a_rmse_lod12.empty()) @@ -1047,7 +1047,7 @@ int main(int argc, const char* argv[]) { building.val3dity_lod12); #endif } - if (handler.cfg_.lod_13) { + if (handler.cfg_.reconstruction.lod13) { ms13 = &building.multisolids_lod13; if (!handler.cfg_.a_rmse_lod13.empty()) attrow.insert_optional(handler.cfg_.a_rmse_lod13, @@ -1061,7 +1061,7 @@ int main(int argc, const char* argv[]) { building.val3dity_lod13); #endif } - if (handler.cfg_.lod_22) { + if (handler.cfg_.reconstruction.lod22) { ms22 = &building.multisolids_lod22; if (!handler.cfg_.a_rmse_lod22.empty()) attrow.insert_optional(handler.cfg_.a_rmse_lod22, diff --git a/apps/roofer-app/validators.hpp b/apps/roofer-app/validators.hpp index 62c8941b..a1ba8b59 100644 --- a/apps/roofer-app/validators.hpp +++ b/apps/roofer-app/validators.hpp @@ -20,65 +20,24 @@ // Ravi Peters // Balázs Dukai #pragma once +#include #include #include - -template -using Validator = std::function(const T&)>; +#include namespace roofer::validators { - // Concept to ensure types are comparable - template - concept Comparable = requires(T a, T b) { - { a < b } -> std::convertible_to; - { a > b } -> std::convertible_to; - }; - - // Generator function for range validators - template - requires Comparable - auto InRange(T min, T max) { - return [min, max](const T& val) -> std::optional { - if (val < min || val > max) { - return std::format("Value {} is out of range <{}, {}>.", val, min, max); - } - return std::nullopt; - }; - }; - - // Generator function for validator to check if a value is higher than a given - // value - template - requires Comparable - auto HigherThan(T min) { - return [min](const T& val) -> std::optional { - if constexpr (std::is_same_v) { - if (val[0] <= min[0] || val[1] <= min[1]) { - return std::format( - "One of the values of [{}, {}] is too low. Values must be higher " - "than {} and {} respectively.", - val[0], val[1], min[0], min[1]); - } - } else if (val <= min) { - return std::format("Value must be higher than {}.", min); - } - return std::nullopt; - }; - }; - - // Generator function for validator to check if a value is higher than or - // equal to a given value - template - requires Comparable - auto HigherOrEqualTo(T min) { - return [min](const T& val) -> std::optional { - if (val < min) { + // Array-specific application validation; scalar bounds use roofer::config. + inline auto AllHigherThan(roofer::arr2f min) { + return [min](const roofer::arr2f& val) -> std::optional { + if (val[0] <= min[0] || val[1] <= min[1]) { return std::format( - "Value must be higher than or equal to {}. But is {}.", min, val); + "One of the values of [{}, {}] is too low. Values must be higher " + "than {} and {} respectively.", + val[0], val[1], min[0], min[1]); } return std::nullopt; }; - }; + } // Generator function for validator to check if the value is one of the given // values diff --git a/docs/reconstruct_params.md b/docs/reconstruct_params.md index 6ac7e550..f9e9dc0b 100644 --- a/docs/reconstruct_params.md +++ b/docs/reconstruct_params.md @@ -32,10 +32,37 @@ The roofer building reconstruction algorithm consists of the following steps: The roofer building reconstruction algorithm is largely data-driven, so the quality of the result depends on the quality of the input data. +## Configuration + +Reconstruction settings live in the `[reconstruction]` TOML table. Settings +for individual stages use nested tables such as +`[reconstruction.plane-detector]` and +`[reconstruction.arrangement-optimiser]`. The complete example configuration +is generated from the C++ field descriptors, so it always reflects the runtime +defaults and validation rules. + +The former root-level reconstruction keys remain compatibility aliases in the +1.x series. They emit deprecation warnings and are scheduled for removal in +2.0. The `--lod12`, `--lod13`, `--lod22`, `--clip-terrain`, and +`--complexity-factor` CLI flags remain supported and are not deprecated. Other +legacy reconstruction CLI flags emit deprecation warnings. Nested TOML values +take precedence over root-level aliases; command-line arguments take +precedence over both. + +The removed `lod11-fallback-time` root key and CLI flag are accepted but +ignored during the 1.x compatibility period and emit a deprecation warning. +Use `max-plane-count` under `[reconstruction.plane-detector]` as the +deterministic reconstruction-complexity cutoff. + +Low-level C++ configuration member names were normalised to `snake_case` in +1.x. The flattened reconstruction API remains available as a deprecated +adapter, but code using component structs directly must use the cleaned names; +those direct component member renames cannot be represented by C++ aliases. +