Сформирован пресет для отрисовки
This commit is contained in:
@@ -39,6 +39,7 @@ contains(DEFINES, PCL_ENABLED) {
|
|||||||
|
|
||||||
SOURCES += \
|
SOURCES += \
|
||||||
src/main.cpp \
|
src/main.cpp \
|
||||||
|
src/cli/pipeline_cli_runner.cpp \
|
||||||
src/algorithms/preprocess/preprocess_algorithms.cpp \
|
src/algorithms/preprocess/preprocess_algorithms.cpp \
|
||||||
src/algorithms/reconstruction/surface_reconstruction.cpp \
|
src/algorithms/reconstruction/surface_reconstruction.cpp \
|
||||||
src/adapters/reconstruction/reconstruction_adapter.cpp \
|
src/adapters/reconstruction/reconstruction_adapter.cpp \
|
||||||
@@ -49,6 +50,10 @@ SOURCES += \
|
|||||||
src/adapters/sources/file_point_cloud_source.cpp \
|
src/adapters/sources/file_point_cloud_source.cpp \
|
||||||
src/adapters/sources/ros2_point_cloud_source.cpp \
|
src/adapters/sources/ros2_point_cloud_source.cpp \
|
||||||
src/core/pipeline_config.cpp \
|
src/core/pipeline_config.cpp \
|
||||||
|
src/core/pipeline_config_validation.cpp \
|
||||||
|
src/core/hole_aware_score.cpp \
|
||||||
|
src/core/pipeline_stage_defaults.cpp \
|
||||||
|
src/core/tuning_service.cpp \
|
||||||
src/core/pipeline_executor.cpp \
|
src/core/pipeline_executor.cpp \
|
||||||
src/core/pipeline_plugin_registry.cpp \
|
src/core/pipeline_plugin_registry.cpp \
|
||||||
src/factories/pipeline/desktop_pipeline_factory.cpp \
|
src/factories/pipeline/desktop_pipeline_factory.cpp \
|
||||||
@@ -61,6 +66,7 @@ SOURCES += \
|
|||||||
src/ui/glview.cpp
|
src/ui/glview.cpp
|
||||||
|
|
||||||
HEADERS += \
|
HEADERS += \
|
||||||
|
src/cli/pipeline_cli_runner.h \
|
||||||
src/algorithms/preprocess/preprocess_algorithms.h \
|
src/algorithms/preprocess/preprocess_algorithms.h \
|
||||||
src/algorithms/reconstruction/surface_reconstruction.h \
|
src/algorithms/reconstruction/surface_reconstruction.h \
|
||||||
src/adapters/reconstruction/reconstruction_adapter.h \
|
src/adapters/reconstruction/reconstruction_adapter.h \
|
||||||
@@ -73,6 +79,10 @@ HEADERS += \
|
|||||||
src/core/data_source.h \
|
src/core/data_source.h \
|
||||||
src/core/point_cloud_types.h \
|
src/core/point_cloud_types.h \
|
||||||
src/core/pipeline_config.h \
|
src/core/pipeline_config.h \
|
||||||
|
src/core/pipeline_config_validation.h \
|
||||||
|
src/core/hole_aware_score.h \
|
||||||
|
src/core/pipeline_stage_defaults.h \
|
||||||
|
src/core/tuning_service.h \
|
||||||
src/core/pipeline_stage.h \
|
src/core/pipeline_stage.h \
|
||||||
src/core/pipeline_executor.h \
|
src/core/pipeline_executor.h \
|
||||||
src/core/pipeline_plugin_registry.h \
|
src/core/pipeline_plugin_registry.h \
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ SOURCES += \
|
|||||||
src/adapters/sources/file_point_cloud_source.cpp \
|
src/adapters/sources/file_point_cloud_source.cpp \
|
||||||
src/adapters/sources/ros2_point_cloud_source.cpp \
|
src/adapters/sources/ros2_point_cloud_source.cpp \
|
||||||
src/core/pipeline_config.cpp \
|
src/core/pipeline_config.cpp \
|
||||||
|
src/core/pipeline_config_validation.cpp \
|
||||||
|
src/core/hole_aware_score.cpp \
|
||||||
|
src/core/pipeline_stage_defaults.cpp \
|
||||||
|
src/core/tuning_service.cpp \
|
||||||
src/core/pipeline_executor.cpp \
|
src/core/pipeline_executor.cpp \
|
||||||
src/core/pipeline_plugin_registry.cpp \
|
src/core/pipeline_plugin_registry.cpp \
|
||||||
src/factories/pipeline/desktop_pipeline_factory.cpp \
|
src/factories/pipeline/desktop_pipeline_factory.cpp \
|
||||||
@@ -70,6 +74,10 @@ HEADERS += \
|
|||||||
src/core/data_source.h \
|
src/core/data_source.h \
|
||||||
src/core/point_cloud_types.h \
|
src/core/point_cloud_types.h \
|
||||||
src/core/pipeline_config.h \
|
src/core/pipeline_config.h \
|
||||||
|
src/core/pipeline_config_validation.h \
|
||||||
|
src/core/hole_aware_score.h \
|
||||||
|
src/core/pipeline_stage_defaults.h \
|
||||||
|
src/core/tuning_service.h \
|
||||||
src/core/pipeline_stage.h \
|
src/core/pipeline_stage.h \
|
||||||
src/core/pipeline_executor.h \
|
src/core/pipeline_executor.h \
|
||||||
src/core/pipeline_plugin_registry.h \
|
src/core/pipeline_plugin_registry.h \
|
||||||
|
|||||||
@@ -35,6 +35,15 @@
|
|||||||
- `strategies`: конкретные стадии пайплайна (preprocess, transform, registration, reconstruction).
|
- `strategies`: конкретные стадии пайплайна (preprocess, transform, registration, reconstruction).
|
||||||
- `factories`: сборка `PipelineExecutor` из plugin-id и профиля.
|
- `factories`: сборка `PipelineExecutor` из plugin-id и профиля.
|
||||||
|
|
||||||
|
## Этап 1 (backend-first, без CLI)
|
||||||
|
|
||||||
|
- Парсинг и применение `defaults` параметров стадий вынесены из UI в `core`:
|
||||||
|
`src/core/pipeline_stage_defaults.h` и `src/core/pipeline_stage_defaults.cpp`.
|
||||||
|
- `MainWindow` больше не содержит backend-логику разбора параметров, а вызывает
|
||||||
|
`core::applyStageDefaultsToConfig(...)`.
|
||||||
|
- Это позволяет использовать один и тот же backend API из GUI сейчас и из будущего
|
||||||
|
CLI на этапе 2 без дублирования логики.
|
||||||
|
|
||||||
## Pipeline Dashboard: UX и анализ цепочки
|
## Pipeline Dashboard: UX и анализ цепочки
|
||||||
|
|
||||||
В dashboard реализованы инструменты для пошаговой настройки всей pipeline-цепочки (preprocess + reconstruction):
|
В dashboard реализованы инструменты для пошаговой настройки всей pipeline-цепочки (preprocess + reconstruction):
|
||||||
@@ -103,6 +112,98 @@ make
|
|||||||
|
|
||||||
При успешном запуске runner печатает `Smoke tests passed.` и завершаетcя с кодом `0`.
|
При успешном запуске runner печатает `Smoke tests passed.` и завершаетcя с кодом `0`.
|
||||||
|
|
||||||
|
## CLI режим (этап 2)
|
||||||
|
|
||||||
|
Для headless-прогона без GUI добавлен CLI-режим в основном бинарнике через `--cli`.
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /mnt/d/yakupov/Projects/DotsToSirface/build-wsl
|
||||||
|
qmake ../DotsToSirface.pro 'DEFINES+=PCL_ENABLED'
|
||||||
|
make -j4
|
||||||
|
./DotsToSirface --cli \
|
||||||
|
--input /mnt/d/path/to/cloud.xyz \
|
||||||
|
--profile desktop_debug \
|
||||||
|
--preprocess pcl_remove_nan,pcl_voxel_grid,pcl_statistical_outlier \
|
||||||
|
--reconstruction pcl_greedy_triangulation \
|
||||||
|
--stage-default pcl_voxel_grid:leaf=0.03 \
|
||||||
|
--stage-default pcl_greedy_triangulation:searchRadius=0.08,mu=2.5,maxNearest=100,maxSurfaceAngle=0.8 \
|
||||||
|
--output-json /mnt/d/path/to/result.json
|
||||||
|
```
|
||||||
|
|
||||||
|
CLI печатает краткую сводку по метрикам (`input`, `after preprocess`, `triangles`, `reconstruction ms`).
|
||||||
|
|
||||||
|
### JSON-конфиг для CLI (этап 2.1)
|
||||||
|
|
||||||
|
Чтобы не передавать длинную команду, можно задать пайплайн через `--config-json`.
|
||||||
|
|
||||||
|
Пример `pipeline_config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"profile": "desktop_debug",
|
||||||
|
"preprocessPlugins": ["pcl_remove_nan", "pcl_voxel_grid", "pcl_statistical_outlier"],
|
||||||
|
"reconstructionPlugin": "pcl_greedy_triangulation",
|
||||||
|
"stageDefaults": {
|
||||||
|
"pcl_voxel_grid": "leaf=0.03",
|
||||||
|
"pcl_greedy_triangulation": "searchRadius=0.08,mu=2.5,maxNearest=100,maxSurfaceAngle=0.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Запуск:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./DotsToSirface --cli --input /mnt/d/path/to/cloud.xyz --config-json /mnt/d/path/to/pipeline_config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Если одновременно переданы `--config-json` и обычные CLI-флаги (`--preprocess`, `--reconstruction`, `--stage-default`), флаги командной строки имеют приоритет.
|
||||||
|
|
||||||
|
### Autotune Grid Search (этап 3)
|
||||||
|
|
||||||
|
Для подбора параметров с целью уменьшения дыр на поверхности добавлен режим:
|
||||||
|
`--autotune-config`.
|
||||||
|
|
||||||
|
Пример `autotune_config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"baseConfigJson": "/mnt/d/path/to/pipeline_config.json",
|
||||||
|
"searchSpace": {
|
||||||
|
"pclVoxelLeafSize": [0.02, 0.03, 0.04],
|
||||||
|
"pclGreedySearchRadius": [0.06, 0.08, 0.10],
|
||||||
|
"neighborRadiusScale": [2.5, 3.0]
|
||||||
|
},
|
||||||
|
"objective": {
|
||||||
|
"connectivityWeight": 0.45,
|
||||||
|
"coverageWeight": 0.45,
|
||||||
|
"speedPenaltyWeight": 0.10
|
||||||
|
},
|
||||||
|
"limits": {
|
||||||
|
"maxCandidates": 20,
|
||||||
|
"topK": 5
|
||||||
|
},
|
||||||
|
"output": {
|
||||||
|
"resultJsonPath": "/mnt/d/path/to/autotune_result.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Запуск:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./DotsToSirface --cli \
|
||||||
|
--input /mnt/d/path/to/cloud.xyz \
|
||||||
|
--autotune-config /mnt/d/path/to/autotune_config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Опционально можно переопределить путь результата через `--output-json`.
|
||||||
|
|
||||||
|
Файл `autotune_result.json` теперь содержит поля `title` и `stages` в формате GUI-пресета,
|
||||||
|
поэтому его можно загрузить напрямую кнопкой `Загрузить пресет`.
|
||||||
|
Дополнительно рядом сохраняется `*_best_preset.json` (чистый preset-JSON).
|
||||||
|
|
||||||
## Демо
|
## Демо
|
||||||
|
|
||||||
При запуске приложение:
|
При запуске приложение:
|
||||||
|
|||||||
@@ -0,0 +1,509 @@
|
|||||||
|
#include "pipeline_cli_runner.h"
|
||||||
|
|
||||||
|
#include <QCommandLineOption>
|
||||||
|
#include <QCommandLineParser>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QTextStream>
|
||||||
|
|
||||||
|
#include "../adapters/sources/file_point_cloud_source.h"
|
||||||
|
#include "../core/pipeline_config.h"
|
||||||
|
#include "../core/pipeline_config_validation.h"
|
||||||
|
#include "../core/pipeline_executor.h"
|
||||||
|
#include "../core/pipeline_stage_defaults.h"
|
||||||
|
#include "../core/tuning_service.h"
|
||||||
|
#include "../factories/pipeline/desktop_pipeline_factory.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
core::PipelineConfig configForProfile(const QString &profile)
|
||||||
|
{
|
||||||
|
if (profile == "rpi4_runtime") {
|
||||||
|
return core::makeRpi4RuntimeConfig();
|
||||||
|
}
|
||||||
|
return core::makeDesktopDebugConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool applyStageDefaultsMap(
|
||||||
|
const QJsonObject &defaultsObj,
|
||||||
|
core::PipelineConfig &config,
|
||||||
|
QString &errorText)
|
||||||
|
{
|
||||||
|
for (auto it = defaultsObj.constBegin(); it != defaultsObj.constEnd(); ++it) {
|
||||||
|
const QString stageId = it.key().trimmed();
|
||||||
|
const QString defaultsText = it.value().toString().trimmed();
|
||||||
|
if (stageId.isEmpty()) {
|
||||||
|
errorText = "Empty stage id in stageDefaults map.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!core::applyStageDefaultsToConfig(stageId, defaultsText, config, errorText)) {
|
||||||
|
errorText = QString("Invalid defaults for stage '%1': %2").arg(stageId, errorText);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool loadConfigFromJsonFile(
|
||||||
|
const QString &path,
|
||||||
|
core::PipelineConfig &config,
|
||||||
|
QString &errorText)
|
||||||
|
{
|
||||||
|
QFile file(path);
|
||||||
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
|
errorText = QString("Cannot open config JSON: %1").arg(path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonParseError parseError;
|
||||||
|
const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &parseError);
|
||||||
|
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||||
|
errorText = QString("Invalid config JSON: %1").arg(parseError.errorString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonObject root = doc.object();
|
||||||
|
if (root.contains("profile")) {
|
||||||
|
config = configForProfile(root.value("profile").toString().trimmed());
|
||||||
|
}
|
||||||
|
if (root.contains("preprocessPlugins")) {
|
||||||
|
config.preprocessPlugins.clear();
|
||||||
|
const QJsonArray arr = root.value("preprocessPlugins").toArray();
|
||||||
|
for (const QJsonValue &v : arr) {
|
||||||
|
const QString id = v.toString().trimmed();
|
||||||
|
if (!id.isEmpty()) {
|
||||||
|
config.preprocessPlugins.push_back(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (root.contains("reconstructionPlugin")) {
|
||||||
|
config.reconstructionPlugin = root.value("reconstructionPlugin").toString().trimmed();
|
||||||
|
}
|
||||||
|
if (root.contains("stageDefaults")) {
|
||||||
|
if (!root.value("stageDefaults").isObject()) {
|
||||||
|
errorText = "Field 'stageDefaults' must be an object: {\"stageId\":\"k=v,...\"}.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
QString defaultsError;
|
||||||
|
if (!applyStageDefaultsMap(root.value("stageDefaults").toObject(), config, defaultsError)) {
|
||||||
|
errorText = defaultsError;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AutotuneCliConfig
|
||||||
|
{
|
||||||
|
QString baseConfigJsonPath;
|
||||||
|
core::AutotuneSearchSpace searchSpace;
|
||||||
|
core::HoleAwareScoreWeights weights;
|
||||||
|
core::AutotuneLimits limits;
|
||||||
|
QString outputPathFromConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool parseFloatArray(const QJsonValue &value, QVector<float> &out, QString &errorText, const QString &fieldName)
|
||||||
|
{
|
||||||
|
if (!value.isArray()) {
|
||||||
|
errorText = QString("Field '%1' must be an array of numbers.").arg(fieldName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const QJsonArray arr = value.toArray();
|
||||||
|
out.clear();
|
||||||
|
for (const QJsonValue &v : arr) {
|
||||||
|
if (!v.isDouble()) {
|
||||||
|
errorText = QString("Field '%1' must contain only numbers.").arg(fieldName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out.push_back(static_cast<float>(v.toDouble()));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool loadAutotuneConfigFromJson(
|
||||||
|
const QString &path,
|
||||||
|
AutotuneCliConfig &cfg,
|
||||||
|
QString &errorText)
|
||||||
|
{
|
||||||
|
QFile file(path);
|
||||||
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
|
errorText = QString("Cannot open autotune JSON: %1").arg(path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonParseError parseError;
|
||||||
|
const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &parseError);
|
||||||
|
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||||
|
errorText = QString("Invalid autotune JSON: %1").arg(parseError.errorString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonObject root = doc.object();
|
||||||
|
cfg = AutotuneCliConfig();
|
||||||
|
cfg.limits.topK = 5;
|
||||||
|
|
||||||
|
if (root.contains("baseConfigJson")) {
|
||||||
|
cfg.baseConfigJsonPath = root.value("baseConfigJson").toString().trimmed();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.contains("searchSpace")) {
|
||||||
|
if (!root.value("searchSpace").isObject()) {
|
||||||
|
errorText = "Field 'searchSpace' must be an object.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const QJsonObject searchObj = root.value("searchSpace").toObject();
|
||||||
|
if (searchObj.contains("pclVoxelLeafSize")
|
||||||
|
&& !parseFloatArray(
|
||||||
|
searchObj.value("pclVoxelLeafSize"),
|
||||||
|
cfg.searchSpace.pclVoxelLeafSizes,
|
||||||
|
errorText,
|
||||||
|
"searchSpace.pclVoxelLeafSize")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (searchObj.contains("pclGreedySearchRadius")
|
||||||
|
&& !parseFloatArray(
|
||||||
|
searchObj.value("pclGreedySearchRadius"),
|
||||||
|
cfg.searchSpace.pclGreedySearchRadii,
|
||||||
|
errorText,
|
||||||
|
"searchSpace.pclGreedySearchRadius")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (searchObj.contains("neighborRadiusScale")
|
||||||
|
&& !parseFloatArray(
|
||||||
|
searchObj.value("neighborRadiusScale"),
|
||||||
|
cfg.searchSpace.neighborRadiusScales,
|
||||||
|
errorText,
|
||||||
|
"searchSpace.neighborRadiusScale")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.contains("objective")) {
|
||||||
|
if (!root.value("objective").isObject()) {
|
||||||
|
errorText = "Field 'objective' must be an object.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const QJsonObject objective = root.value("objective").toObject();
|
||||||
|
if (objective.contains("connectivityWeight")) {
|
||||||
|
cfg.weights.connectivityWeight = objective.value("connectivityWeight").toDouble(cfg.weights.connectivityWeight);
|
||||||
|
}
|
||||||
|
if (objective.contains("coverageWeight")) {
|
||||||
|
cfg.weights.coverageWeight = objective.value("coverageWeight").toDouble(cfg.weights.coverageWeight);
|
||||||
|
}
|
||||||
|
if (objective.contains("speedPenaltyWeight")) {
|
||||||
|
cfg.weights.speedPenaltyWeight = objective.value("speedPenaltyWeight").toDouble(cfg.weights.speedPenaltyWeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.contains("limits")) {
|
||||||
|
if (!root.value("limits").isObject()) {
|
||||||
|
errorText = "Field 'limits' must be an object.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const QJsonObject limitsObj = root.value("limits").toObject();
|
||||||
|
if (limitsObj.contains("maxCandidates")) {
|
||||||
|
cfg.limits.maxCandidates = limitsObj.value("maxCandidates").toInt(cfg.limits.maxCandidates);
|
||||||
|
}
|
||||||
|
if (limitsObj.contains("topK")) {
|
||||||
|
cfg.limits.topK = limitsObj.value("topK").toInt(cfg.limits.topK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.contains("output")) {
|
||||||
|
if (!root.value("output").isObject()) {
|
||||||
|
errorText = "Field 'output' must be an object.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const QJsonObject outputObj = root.value("output").toObject();
|
||||||
|
cfg.outputPathFromConfig = outputObj.value("resultJsonPath").toString().trimmed();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString defaultsForStageInPreset(const QString &stageId, const core::PipelineConfig &config)
|
||||||
|
{
|
||||||
|
if (stageId == "pcl_voxel_grid") {
|
||||||
|
return QString("leaf=%1").arg(config.preprocess.pclVoxelLeafSize);
|
||||||
|
}
|
||||||
|
if (stageId == "pcl_greedy_triangulation") {
|
||||||
|
return QString("searchRadius=%1,mu=%2,maxNearest=%3,maxSurfaceAngle=%4")
|
||||||
|
.arg(config.reconstruction.pclGreedySearchRadius)
|
||||||
|
.arg(config.reconstruction.pclGreedyMu)
|
||||||
|
.arg(config.reconstruction.pclGreedyMaxNearest)
|
||||||
|
.arg(config.reconstruction.pclGreedyMaxSurfaceAngle);
|
||||||
|
}
|
||||||
|
if (stageId == "surface_fallback") {
|
||||||
|
return QString("neighborRadiusScale=%1,runEveryNthFrame=%2")
|
||||||
|
.arg(config.reconstruction.neighborRadiusScale)
|
||||||
|
.arg(config.reconstruction.runEveryNthFrame);
|
||||||
|
}
|
||||||
|
if (stageId == "pcl_statistical_outlier") {
|
||||||
|
return QString("meanK=%1,stddev=%2")
|
||||||
|
.arg(config.preprocess.pclSorMeanK)
|
||||||
|
.arg(config.preprocess.pclSorStdDevMul);
|
||||||
|
}
|
||||||
|
if (stageId == "pcl_radius_outlier") {
|
||||||
|
return QString("radius=%1,minNeighbors=%2")
|
||||||
|
.arg(config.preprocess.pclRorRadius)
|
||||||
|
.arg(config.preprocess.pclRorMinNeighbors);
|
||||||
|
}
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject makePresetFromConfig(const core::PipelineConfig &config, const QString &title)
|
||||||
|
{
|
||||||
|
QJsonArray stages;
|
||||||
|
for (const QString &stageId : config.preprocessPlugins) {
|
||||||
|
QJsonObject stage;
|
||||||
|
stage["id"] = stageId;
|
||||||
|
stage["enabled"] = true;
|
||||||
|
stage["family"] = "preprocess";
|
||||||
|
const QString defaults = defaultsForStageInPreset(stageId, config);
|
||||||
|
if (!defaults.isEmpty()) {
|
||||||
|
stage["defaults"] = defaults;
|
||||||
|
}
|
||||||
|
stages.push_back(stage);
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject reconstruction;
|
||||||
|
reconstruction["id"] = config.reconstructionPlugin;
|
||||||
|
reconstruction["enabled"] = true;
|
||||||
|
reconstruction["family"] = "reconstruction";
|
||||||
|
const QString reconstructionDefaults = defaultsForStageInPreset(config.reconstructionPlugin, config);
|
||||||
|
if (!reconstructionDefaults.isEmpty()) {
|
||||||
|
reconstruction["defaults"] = reconstructionDefaults;
|
||||||
|
}
|
||||||
|
stages.push_back(reconstruction);
|
||||||
|
|
||||||
|
QJsonObject preset;
|
||||||
|
preset["title"] = title;
|
||||||
|
preset["idValue"] = "user:autotune_best";
|
||||||
|
preset["stages"] = stages;
|
||||||
|
return preset;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int runCliPipeline(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
QCoreApplication app(argc, argv);
|
||||||
|
app.setApplicationName("DotsToSirface");
|
||||||
|
|
||||||
|
QCommandLineParser parser;
|
||||||
|
parser.setApplicationDescription("DotsToSirface CLI pipeline runner");
|
||||||
|
parser.addHelpOption();
|
||||||
|
parser.addOption(QCommandLineOption("cli", "Run in CLI mode."));
|
||||||
|
parser.addOption(QCommandLineOption(QStringList() << "i" << "input", "Input point cloud file path.", "path"));
|
||||||
|
parser.addOption(QCommandLineOption("config-json", "Path to pipeline JSON config file.", "path"));
|
||||||
|
parser.addOption(QCommandLineOption("autotune-config", "Path to autotune JSON config file.", "path"));
|
||||||
|
parser.addOption(QCommandLineOption("profile", "Pipeline profile: desktop_debug or rpi4_runtime.", "name", "desktop_debug"));
|
||||||
|
parser.addOption(QCommandLineOption("preprocess", "Comma-separated preprocess plugin ids.", "ids"));
|
||||||
|
parser.addOption(QCommandLineOption("reconstruction", "Reconstruction plugin id.", "id"));
|
||||||
|
parser.addOption(QCommandLineOption(
|
||||||
|
"stage-default",
|
||||||
|
"Stage parameter override as stageId:key=value,key2=value2. Can be repeated.",
|
||||||
|
"spec"));
|
||||||
|
parser.addOption(QCommandLineOption("output-json", "Write run summary JSON to this file.", "path"));
|
||||||
|
parser.process(app);
|
||||||
|
|
||||||
|
QTextStream out(stdout);
|
||||||
|
QTextStream err(stderr);
|
||||||
|
|
||||||
|
const QString inputPath = parser.value("input").trimmed();
|
||||||
|
if (inputPath.isEmpty()) {
|
||||||
|
err << "--input is required in --cli mode.\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
core::PipelineConfig config = configForProfile(parser.value("profile").trimmed());
|
||||||
|
const QString configJsonPath = parser.value("config-json").trimmed();
|
||||||
|
if (!configJsonPath.isEmpty()) {
|
||||||
|
QString jsonError;
|
||||||
|
if (!loadConfigFromJsonFile(configJsonPath, config, jsonError)) {
|
||||||
|
err << jsonError << "\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString autotuneConfigPath = parser.value("autotune-config").trimmed();
|
||||||
|
if (!autotuneConfigPath.isEmpty()) {
|
||||||
|
AutotuneCliConfig autotuneCfg;
|
||||||
|
QString autotuneError;
|
||||||
|
if (!loadAutotuneConfigFromJson(autotuneConfigPath, autotuneCfg, autotuneError)) {
|
||||||
|
err << autotuneError << "\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!autotuneCfg.baseConfigJsonPath.isEmpty()) {
|
||||||
|
QString baseError;
|
||||||
|
if (!loadConfigFromJsonFile(autotuneCfg.baseConfigJsonPath, config, baseError)) {
|
||||||
|
err << baseError << "\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString configError;
|
||||||
|
if (!core::normalizeAndValidatePipelineConfig(config, configError)) {
|
||||||
|
err << "Invalid base config: " << configError << "\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
adapters::sources::FilePointCloudSource source(inputPath);
|
||||||
|
core::PointCloudFrame frame;
|
||||||
|
QString ioError;
|
||||||
|
if (!source.nextFrame(frame, ioError)) {
|
||||||
|
err << "Failed to read input point cloud: " << ioError << "\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const core::AutotuneResult tuneResult = core::runGridAutotune(
|
||||||
|
frame,
|
||||||
|
config,
|
||||||
|
autotuneCfg.searchSpace,
|
||||||
|
autotuneCfg.weights,
|
||||||
|
autotuneCfg.limits);
|
||||||
|
if (!tuneResult.ok) {
|
||||||
|
err << "Autotune failed: " << tuneResult.errorText << "\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
out << "Autotune candidates evaluated: " << tuneResult.evaluatedCandidates << "\n";
|
||||||
|
out << "Best score: " << tuneResult.best.score << "\n";
|
||||||
|
out << "Best pipeline: PP=[" << tuneResult.best.config.preprocessPlugins.join("->")
|
||||||
|
<< "] REC=" << tuneResult.best.config.reconstructionPlugin << "\n";
|
||||||
|
out << "Best params: leaf=" << tuneResult.best.config.preprocess.pclVoxelLeafSize
|
||||||
|
<< " greedyRadius=" << tuneResult.best.config.reconstruction.pclGreedySearchRadius
|
||||||
|
<< " neighborRadiusScale=" << tuneResult.best.config.reconstruction.neighborRadiusScale << "\n";
|
||||||
|
|
||||||
|
QString outputJsonPath = parser.value("output-json").trimmed();
|
||||||
|
if (outputJsonPath.isEmpty()) {
|
||||||
|
outputJsonPath = autotuneCfg.outputPathFromConfig;
|
||||||
|
}
|
||||||
|
if (!outputJsonPath.isEmpty()) {
|
||||||
|
QJsonObject bestObj;
|
||||||
|
bestObj["score"] = tuneResult.best.score;
|
||||||
|
bestObj["pclVoxelLeafSize"] = tuneResult.best.config.preprocess.pclVoxelLeafSize;
|
||||||
|
bestObj["pclGreedySearchRadius"] = tuneResult.best.config.reconstruction.pclGreedySearchRadius;
|
||||||
|
bestObj["neighborRadiusScale"] = tuneResult.best.config.reconstruction.neighborRadiusScale;
|
||||||
|
bestObj["inputPoints"] = tuneResult.best.inputPoints;
|
||||||
|
bestObj["afterPreprocess"] = tuneResult.best.afterPreprocessPoints;
|
||||||
|
bestObj["triangles"] = tuneResult.best.outputTriangles;
|
||||||
|
bestObj["reconstructionMs"] = static_cast<qint64>(tuneResult.best.reconstructionMs);
|
||||||
|
|
||||||
|
QJsonArray topArray;
|
||||||
|
for (const core::AutotuneCandidateResult &candidate : tuneResult.topCandidates) {
|
||||||
|
QJsonObject c;
|
||||||
|
c["score"] = candidate.score;
|
||||||
|
c["pclVoxelLeafSize"] = candidate.config.preprocess.pclVoxelLeafSize;
|
||||||
|
c["pclGreedySearchRadius"] = candidate.config.reconstruction.pclGreedySearchRadius;
|
||||||
|
c["neighborRadiusScale"] = candidate.config.reconstruction.neighborRadiusScale;
|
||||||
|
c["triangles"] = candidate.outputTriangles;
|
||||||
|
c["afterPreprocess"] = candidate.afterPreprocessPoints;
|
||||||
|
c["reconstructionMs"] = static_cast<qint64>(candidate.reconstructionMs);
|
||||||
|
topArray.push_back(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonObject bestPreset = makePresetFromConfig(tuneResult.best.config, "AutoTune Best");
|
||||||
|
QJsonObject root;
|
||||||
|
root["title"] = bestPreset.value("title");
|
||||||
|
root["idValue"] = bestPreset.value("idValue");
|
||||||
|
root["stages"] = bestPreset.value("stages");
|
||||||
|
root["evaluatedCandidates"] = tuneResult.evaluatedCandidates;
|
||||||
|
root["best"] = bestObj;
|
||||||
|
root["topK"] = topArray;
|
||||||
|
root["bestPreset"] = bestPreset;
|
||||||
|
|
||||||
|
QFile file(outputJsonPath);
|
||||||
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
||||||
|
err << "Failed to open output JSON file: " << outputJsonPath << "\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
file.write(QJsonDocument(root).toJson(QJsonDocument::Indented));
|
||||||
|
|
||||||
|
const QFileInfo info(outputJsonPath);
|
||||||
|
const QString presetPath = info.path() + "/" + info.completeBaseName() + "_best_preset.json";
|
||||||
|
QFile presetFile(presetPath);
|
||||||
|
if (presetFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
||||||
|
presetFile.write(QJsonDocument(bestPreset).toJson(QJsonDocument::Indented));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString preprocessRaw = parser.value("preprocess").trimmed();
|
||||||
|
if (!preprocessRaw.isEmpty()) {
|
||||||
|
config.preprocessPlugins = preprocessRaw.split(',', Qt::SkipEmptyParts);
|
||||||
|
for (int i = 0; i < config.preprocessPlugins.size(); ++i) {
|
||||||
|
config.preprocessPlugins[i] = config.preprocessPlugins[i].trimmed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString reconstruction = parser.value("reconstruction").trimmed();
|
||||||
|
if (!reconstruction.isEmpty()) {
|
||||||
|
config.reconstructionPlugin = reconstruction;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QStringList defaultsSpecs = parser.values("stage-default");
|
||||||
|
for (const QString &specRaw : defaultsSpecs) {
|
||||||
|
const QString spec = specRaw.trimmed();
|
||||||
|
const int colonPos = spec.indexOf(':');
|
||||||
|
if (colonPos <= 0 || colonPos >= spec.size() - 1) {
|
||||||
|
err << "Invalid --stage-default format, expected stageId:key=value,... got: " << spec << "\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
const QString stageId = spec.left(colonPos).trimmed();
|
||||||
|
const QString defaultsText = spec.mid(colonPos + 1).trimmed();
|
||||||
|
QString errorText;
|
||||||
|
if (!core::applyStageDefaultsToConfig(stageId, defaultsText, config, errorText)) {
|
||||||
|
err << "Invalid defaults for stage '" << stageId << "': " << errorText << "\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString configError;
|
||||||
|
if (!core::normalizeAndValidatePipelineConfig(config, configError)) {
|
||||||
|
err << "Invalid pipeline config: " << configError << "\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
adapters::sources::FilePointCloudSource source(inputPath);
|
||||||
|
core::PointCloudFrame frame;
|
||||||
|
QString ioError;
|
||||||
|
if (!source.nextFrame(frame, ioError)) {
|
||||||
|
err << "Failed to read input point cloud: " << ioError << "\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
core::PipelineExecutor executor = factories::pipeline::createDesktopPipelineExecutor(config);
|
||||||
|
const core::PipelineResult result = executor.run(frame);
|
||||||
|
|
||||||
|
out << "Input points: " << result.stats.inputPoints << "\n";
|
||||||
|
out << "After preprocess: " << result.stats.afterPreprocessingPoints << "\n";
|
||||||
|
out << "Triangles: " << result.stats.outputTriangles << "\n";
|
||||||
|
out << "Reconstruction ms: " << result.stats.reconstructionMs << "\n";
|
||||||
|
out << "Pipeline: PP=[" << config.preprocessPlugins.join("->") << "] REC=" << config.reconstructionPlugin << "\n";
|
||||||
|
|
||||||
|
const QString outputJsonPath = parser.value("output-json").trimmed();
|
||||||
|
if (!outputJsonPath.isEmpty()) {
|
||||||
|
QJsonObject root;
|
||||||
|
root["inputPoints"] = result.stats.inputPoints;
|
||||||
|
root["afterPreprocess"] = result.stats.afterPreprocessingPoints;
|
||||||
|
root["triangles"] = result.stats.outputTriangles;
|
||||||
|
root["reconstructionMs"] = static_cast<qint64>(result.stats.reconstructionMs);
|
||||||
|
root["reconstruction"] = config.reconstructionPlugin;
|
||||||
|
root["preprocessChain"] = config.preprocessPlugins.join(",");
|
||||||
|
|
||||||
|
QFile file(outputJsonPath);
|
||||||
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
||||||
|
err << "Failed to open output JSON file: " << outputJsonPath << "\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
file.write(QJsonDocument(root).toJson(QJsonDocument::Indented));
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#ifndef PIPELINE_CLI_RUNNER_H
|
||||||
|
#define PIPELINE_CLI_RUNNER_H
|
||||||
|
|
||||||
|
int runCliPipeline(int argc, char *argv[]);
|
||||||
|
|
||||||
|
#endif // PIPELINE_CLI_RUNNER_H
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#include "hole_aware_score.h"
|
||||||
|
|
||||||
|
#include <QtMath>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
double clamp01(const double v)
|
||||||
|
{
|
||||||
|
if (v < 0.0) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
if (v > 1.0) {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace core
|
||||||
|
{
|
||||||
|
HoleAwareScoreBreakdown computeHoleAwareScore(
|
||||||
|
const PipelineResult &result,
|
||||||
|
const HoleAwareScoreWeights &weights)
|
||||||
|
{
|
||||||
|
HoleAwareScoreBreakdown score;
|
||||||
|
const double inputPoints = qMax(1, result.stats.inputPoints);
|
||||||
|
const double processedPoints = qMax(1, result.stats.afterPreprocessingPoints);
|
||||||
|
const double triangles = qMax(0, result.stats.outputTriangles);
|
||||||
|
|
||||||
|
// Proxy 1: low triangle density often corresponds to sparse surfaces and visible holes.
|
||||||
|
const double triangleDensity = triangles / processedPoints;
|
||||||
|
const double targetDensity = 0.80;
|
||||||
|
score.connectivityScore = clamp01(triangleDensity / targetDensity);
|
||||||
|
|
||||||
|
// Proxy 2: aggressive filtering can remove support points and increase hole probability.
|
||||||
|
score.coverageScore = clamp01(processedPoints / inputPoints);
|
||||||
|
|
||||||
|
// Secondary: discourage very slow reconstructions, but keep it lower priority.
|
||||||
|
const double targetMs = 120.0;
|
||||||
|
score.speedPenalty = clamp01(result.stats.reconstructionMs / targetMs);
|
||||||
|
|
||||||
|
score.totalScore =
|
||||||
|
weights.connectivityWeight * score.connectivityScore
|
||||||
|
+ weights.coverageWeight * score.coverageScore
|
||||||
|
- weights.speedPenaltyWeight * score.speedPenalty;
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
} // namespace core
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#ifndef HOLE_AWARE_SCORE_H
|
||||||
|
#define HOLE_AWARE_SCORE_H
|
||||||
|
|
||||||
|
#include "point_cloud_types.h"
|
||||||
|
|
||||||
|
namespace core
|
||||||
|
{
|
||||||
|
struct HoleAwareScoreWeights
|
||||||
|
{
|
||||||
|
double connectivityWeight = 0.45;
|
||||||
|
double coverageWeight = 0.45;
|
||||||
|
double speedPenaltyWeight = 0.10;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HoleAwareScoreBreakdown
|
||||||
|
{
|
||||||
|
double connectivityScore = 0.0;
|
||||||
|
double coverageScore = 0.0;
|
||||||
|
double speedPenalty = 0.0;
|
||||||
|
double totalScore = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
HoleAwareScoreBreakdown computeHoleAwareScore(
|
||||||
|
const PipelineResult &result,
|
||||||
|
const HoleAwareScoreWeights &weights);
|
||||||
|
} // namespace core
|
||||||
|
|
||||||
|
#endif // HOLE_AWARE_SCORE_H
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
#include "pipeline_config_validation.h"
|
||||||
|
|
||||||
|
#include <QSet>
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
|
namespace core
|
||||||
|
{
|
||||||
|
bool normalizeAndValidatePipelineConfig(PipelineConfig &config, QString &errorText)
|
||||||
|
{
|
||||||
|
QSet<QString> seen;
|
||||||
|
QStringList normalized;
|
||||||
|
const QStringList allowed = QStringList()
|
||||||
|
<< "keep_largest_cluster"
|
||||||
|
<< "pcl_remove_nan"
|
||||||
|
<< "pcl_remove_nan_normals"
|
||||||
|
<< "pcl_pass_through"
|
||||||
|
<< "pcl_crop_box"
|
||||||
|
<< "pcl_crop_hull"
|
||||||
|
<< "pcl_frustum_culling"
|
||||||
|
<< "pcl_plane_clipper_3d"
|
||||||
|
<< "pcl_conditional_removal"
|
||||||
|
<< "pcl_extract_indices"
|
||||||
|
<< "pcl_functor_filter"
|
||||||
|
<< "pcl_project_inliers"
|
||||||
|
<< "pcl_normal_refinement"
|
||||||
|
<< "pcl_bilateral_filter"
|
||||||
|
<< "pcl_fast_bilateral_filter"
|
||||||
|
<< "pcl_fast_bilateral_filter_omp"
|
||||||
|
<< "pcl_convolution"
|
||||||
|
<< "pcl_gaussian_kernel"
|
||||||
|
<< "pcl_gaussian_kernel_rgb"
|
||||||
|
<< "pcl_voxel_grid_occlusion"
|
||||||
|
<< "downsample_dense"
|
||||||
|
<< "pcl_voxel_grid"
|
||||||
|
<< "pcl_statistical_outlier"
|
||||||
|
<< "pcl_radius_outlier"
|
||||||
|
<< "pcl_model_outlier"
|
||||||
|
<< "pcl_shadow_points"
|
||||||
|
<< "pcl_approximate_voxel_grid"
|
||||||
|
<< "pcl_voxel_grid_label"
|
||||||
|
<< "pcl_voxel_grid_covariance"
|
||||||
|
<< "pcl_grid_minimum"
|
||||||
|
<< "pcl_farthest_point_sampling"
|
||||||
|
<< "pcl_normal_space_sampling"
|
||||||
|
<< "pcl_sampling_surface_normal";
|
||||||
|
for (const QString &id : config.preprocessPlugins) {
|
||||||
|
if (id.isEmpty() || seen.contains(id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!allowed.contains(id)) {
|
||||||
|
errorText = QString("Unknown preprocess plugin: %1").arg(id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
seen.insert(id);
|
||||||
|
normalized.push_back(id);
|
||||||
|
}
|
||||||
|
if (normalized.isEmpty()) {
|
||||||
|
normalized = QStringList() << "keep_largest_cluster" << "downsample_dense";
|
||||||
|
}
|
||||||
|
config.preprocessPlugins = normalized;
|
||||||
|
|
||||||
|
const QStringList reconAllowed = QStringList() << "surface_fallback" << "pcl_greedy_triangulation" << "pcl_poisson_reconstruction";
|
||||||
|
if (!reconAllowed.contains(config.reconstructionPlugin)) {
|
||||||
|
errorText = QString("Unknown reconstruction plugin: %1").arg(config.reconstructionPlugin);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#ifndef PCL_ENABLED
|
||||||
|
for (int i = 0; i < config.preprocessPlugins.size(); ++i) {
|
||||||
|
if (config.preprocessPlugins[i].startsWith("pcl_")) {
|
||||||
|
config.preprocessPlugins[i] = "downsample_dense";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (config.reconstructionPlugin.startsWith("pcl_")) {
|
||||||
|
config.reconstructionPlugin = "surface_fallback";
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} // namespace core
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#ifndef PIPELINE_CONFIG_VALIDATION_H
|
||||||
|
#define PIPELINE_CONFIG_VALIDATION_H
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include "pipeline_config.h"
|
||||||
|
|
||||||
|
namespace core
|
||||||
|
{
|
||||||
|
bool normalizeAndValidatePipelineConfig(PipelineConfig &config, QString &errorText);
|
||||||
|
} // namespace core
|
||||||
|
|
||||||
|
#endif // PIPELINE_CONFIG_VALIDATION_H
|
||||||
@@ -0,0 +1,341 @@
|
|||||||
|
#include "pipeline_stage_defaults.h"
|
||||||
|
|
||||||
|
#include <QMap>
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
QMap<QString, QString> parseDefaultsMap(const QString &defaultsText)
|
||||||
|
{
|
||||||
|
QMap<QString, QString> parsed;
|
||||||
|
const QStringList chunks = defaultsText.split(',', Qt::SkipEmptyParts);
|
||||||
|
for (const QString &chunkRaw : chunks) {
|
||||||
|
const QString chunk = chunkRaw.trimmed();
|
||||||
|
if (chunk.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const int pos = chunk.indexOf('=');
|
||||||
|
if (pos <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const QString key = chunk.left(pos).trimmed();
|
||||||
|
const QString value = chunk.mid(pos + 1).trimmed();
|
||||||
|
if (!key.isEmpty()) {
|
||||||
|
parsed.insert(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool parseIntParam(const QMap<QString, QString> &parsed, const QString &key, int &target, QString &errorText)
|
||||||
|
{
|
||||||
|
if (!parsed.contains(key)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bool ok = false;
|
||||||
|
const int value = parsed.value(key).toInt(&ok);
|
||||||
|
if (!ok) {
|
||||||
|
errorText = QString("Параметр '%1' должен быть целым числом.").arg(key);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
target = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool parseFloatParam(const QMap<QString, QString> &parsed, const QString &key, float &target, QString &errorText)
|
||||||
|
{
|
||||||
|
if (!parsed.contains(key)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bool ok = false;
|
||||||
|
const float value = parsed.value(key).toFloat(&ok);
|
||||||
|
if (!ok) {
|
||||||
|
errorText = QString("Параметр '%1' должен быть числом.").arg(key);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
target = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool parseDoubleParam(const QMap<QString, QString> &parsed, const QString &key, double &target, QString &errorText)
|
||||||
|
{
|
||||||
|
if (!parsed.contains(key)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bool ok = false;
|
||||||
|
const double value = parsed.value(key).toDouble(&ok);
|
||||||
|
if (!ok) {
|
||||||
|
errorText = QString("Параметр '%1' должен быть числом.").arg(key);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
target = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace core
|
||||||
|
{
|
||||||
|
bool applyStageDefaultsToConfig(
|
||||||
|
const QString &stageId,
|
||||||
|
const QString &defaultsText,
|
||||||
|
PipelineConfig &cfg,
|
||||||
|
QString &errorText)
|
||||||
|
{
|
||||||
|
const QMap<QString, QString> parsed = parseDefaultsMap(defaultsText);
|
||||||
|
if (parsed.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stageId == "keep_largest_cluster") {
|
||||||
|
return parseDoubleParam(parsed, "clusterJoinDistanceScale", cfg.preprocess.clusterJoinDistanceScale, errorText);
|
||||||
|
}
|
||||||
|
if (stageId == "downsample_dense") {
|
||||||
|
return parseDoubleParam(parsed, "downsampleCellScale", cfg.preprocess.downsampleCellScale, errorText);
|
||||||
|
}
|
||||||
|
if (stageId == "pcl_voxel_grid") {
|
||||||
|
float leaf = cfg.preprocess.pclVoxelLeafSize;
|
||||||
|
if (!parseFloatParam(parsed, "leaf", leaf, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!parseFloatParam(parsed, "pclVoxelLeafSize", leaf, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
cfg.preprocess.pclVoxelLeafSize = leaf;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (stageId == "pcl_statistical_outlier") {
|
||||||
|
if (!parseIntParam(parsed, "meanK", cfg.preprocess.pclSorMeanK, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!parseDoubleParam(parsed, "stddev", cfg.preprocess.pclSorStdDevMul, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (stageId == "pcl_radius_outlier") {
|
||||||
|
if (!parseFloatParam(parsed, "radius", cfg.preprocess.pclRorRadius, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!parseIntParam(parsed, "minNeighbors", cfg.preprocess.pclRorMinNeighbors, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (stageId == "pcl_model_outlier") {
|
||||||
|
if (!parseFloatParam(parsed, "threshold", cfg.preprocess.pclMorThreshold, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!parseFloatParam(parsed, "pclMorThreshold", cfg.preprocess.pclMorThreshold, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (stageId == "pcl_shadow_points") {
|
||||||
|
if (!parseFloatParam(parsed, "shadowThreshold", cfg.preprocess.pclShadowThreshold, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!parseFloatParam(parsed, "pclShadowThreshold", cfg.preprocess.pclShadowThreshold, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (stageId == "pcl_approximate_voxel_grid") {
|
||||||
|
if (!parseFloatParam(parsed, "leaf", cfg.preprocess.pclApproxLeafSize, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!parseFloatParam(parsed, "pclApproxLeafSize", cfg.preprocess.pclApproxLeafSize, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (stageId == "pcl_voxel_grid_label") {
|
||||||
|
if (!parseFloatParam(parsed, "leaf", cfg.preprocess.pclLabelLeafSize, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!parseFloatParam(parsed, "pclLabelLeafSize", cfg.preprocess.pclLabelLeafSize, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (stageId == "pcl_voxel_grid_covariance") {
|
||||||
|
if (!parseFloatParam(parsed, "leaf", cfg.preprocess.pclCovLeafSize, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!parseFloatParam(parsed, "pclCovLeafSize", cfg.preprocess.pclCovLeafSize, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (stageId == "pcl_grid_minimum") {
|
||||||
|
if (!parseFloatParam(parsed, "resolution", cfg.preprocess.pclGridMinimumResolution, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!parseFloatParam(parsed, "pclGridMinimumResolution", cfg.preprocess.pclGridMinimumResolution, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (stageId == "pcl_farthest_point_sampling") {
|
||||||
|
if (!parseIntParam(parsed, "sample", cfg.preprocess.pclFpsSampleCount, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!parseIntParam(parsed, "pclFpsSampleCount", cfg.preprocess.pclFpsSampleCount, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (stageId == "pcl_normal_space_sampling") {
|
||||||
|
if (!parseIntParam(parsed, "sample", cfg.preprocess.pclNormalSpaceSampleCount, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!parseIntParam(parsed, "pclNormalSpaceSampleCount", cfg.preprocess.pclNormalSpaceSampleCount, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (stageId == "pcl_sampling_surface_normal") {
|
||||||
|
if (!parseIntParam(parsed, "sample", cfg.preprocess.pclSurfaceNormalSampleCount, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!parseIntParam(parsed, "pclSurfaceNormalSampleCount", cfg.preprocess.pclSurfaceNormalSampleCount, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (stageId == "pcl_pass_through") {
|
||||||
|
if (parsed.contains("axis")) {
|
||||||
|
const QString axis = parsed.value("axis").toLower();
|
||||||
|
if (axis != "x" && axis != "y" && axis != "z") {
|
||||||
|
errorText = "Параметр 'axis' должен быть x, y или z.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
cfg.preprocess.pclPassAxis = axis;
|
||||||
|
}
|
||||||
|
if (!parseFloatParam(parsed, "min", cfg.preprocess.pclPassMin, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!parseFloatParam(parsed, "max", cfg.preprocess.pclPassMax, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (stageId == "pcl_crop_box" || stageId == "pcl_crop_hull") {
|
||||||
|
if (!parseFloatParam(parsed, "minX", cfg.preprocess.pclCropBoxMinX, errorText)
|
||||||
|
|| !parseFloatParam(parsed, "minY", cfg.preprocess.pclCropBoxMinY, errorText)
|
||||||
|
|| !parseFloatParam(parsed, "minZ", cfg.preprocess.pclCropBoxMinZ, errorText)
|
||||||
|
|| !parseFloatParam(parsed, "maxX", cfg.preprocess.pclCropBoxMaxX, errorText)
|
||||||
|
|| !parseFloatParam(parsed, "maxY", cfg.preprocess.pclCropBoxMaxY, errorText)
|
||||||
|
|| !parseFloatParam(parsed, "maxZ", cfg.preprocess.pclCropBoxMaxZ, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (stageId == "pcl_frustum_culling") {
|
||||||
|
if (!parseFloatParam(parsed, "near", cfg.preprocess.pclFrustumNear, errorText)
|
||||||
|
|| !parseFloatParam(parsed, "far", cfg.preprocess.pclFrustumFar, errorText)
|
||||||
|
|| !parseFloatParam(parsed, "hfov", cfg.preprocess.pclFrustumHfovDeg, errorText)
|
||||||
|
|| !parseFloatParam(parsed, "vfov", cfg.preprocess.pclFrustumVfovDeg, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (stageId == "pcl_plane_clipper_3d") {
|
||||||
|
if (!parseFloatParam(parsed, "a", cfg.preprocess.pclClipPlaneA, errorText)
|
||||||
|
|| !parseFloatParam(parsed, "b", cfg.preprocess.pclClipPlaneB, errorText)
|
||||||
|
|| !parseFloatParam(parsed, "c", cfg.preprocess.pclClipPlaneC, errorText)
|
||||||
|
|| !parseFloatParam(parsed, "d", cfg.preprocess.pclClipPlaneD, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (parsed.contains("keepPositive")) {
|
||||||
|
const QString v = parsed.value("keepPositive").trimmed().toLower();
|
||||||
|
if (v != "true" && v != "false") {
|
||||||
|
errorText = "Параметр 'keepPositive' должен быть true или false.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
cfg.preprocess.pclClipKeepPositive = (v == "true");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (stageId == "pcl_conditional_removal") {
|
||||||
|
if (!parseFloatParam(parsed, "zMin", cfg.preprocess.pclConditionalZMin, errorText)
|
||||||
|
|| !parseFloatParam(parsed, "zMax", cfg.preprocess.pclConditionalZMax, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (stageId == "pcl_extract_indices") {
|
||||||
|
if (!parseIntParam(parsed, "nth", cfg.preprocess.pclExtractNth, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (stageId == "pcl_functor_filter") {
|
||||||
|
if (!parseFloatParam(parsed, "radiusMax", cfg.preprocess.pclFunctorRadiusMax, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (stageId == "pcl_project_inliers") {
|
||||||
|
if (!parseFloatParam(parsed, "a", cfg.preprocess.pclProjectPlaneA, errorText)
|
||||||
|
|| !parseFloatParam(parsed, "b", cfg.preprocess.pclProjectPlaneB, errorText)
|
||||||
|
|| !parseFloatParam(parsed, "c", cfg.preprocess.pclProjectPlaneC, errorText)
|
||||||
|
|| !parseFloatParam(parsed, "d", cfg.preprocess.pclProjectPlaneD, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (stageId == "pcl_normal_refinement") {
|
||||||
|
if (!parseFloatParam(parsed, "radius", cfg.preprocess.pclNormalRefineRadius, errorText)
|
||||||
|
|| !parseIntParam(parsed, "iterations", cfg.preprocess.pclNormalRefineIterations, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (stageId == "pcl_bilateral_filter" || stageId == "pcl_fast_bilateral_filter" || stageId == "pcl_fast_bilateral_filter_omp") {
|
||||||
|
if (!parseFloatParam(parsed, "sigmaS", cfg.preprocess.pclBilateralSigmaS, errorText)
|
||||||
|
|| !parseFloatParam(parsed, "sigmaR", cfg.preprocess.pclBilateralSigmaR, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (stageId == "pcl_convolution" || stageId == "pcl_gaussian_kernel" || stageId == "pcl_gaussian_kernel_rgb") {
|
||||||
|
if (!parseFloatParam(parsed, "sigma", cfg.preprocess.pclConvolutionKernelSigma, errorText)
|
||||||
|
|| !parseIntParam(parsed, "kernel", cfg.preprocess.pclConvolutionKernelSize, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (stageId == "pcl_voxel_grid_occlusion") {
|
||||||
|
if (!parseFloatParam(parsed, "leaf", cfg.preprocess.pclVoxelOccLeaf, errorText)
|
||||||
|
|| !parseIntParam(parsed, "minHits", cfg.preprocess.pclVoxelOccMinHits, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (stageId == "surface_fallback") {
|
||||||
|
if (!parseFloatParam(parsed, "neighborRadiusScale", cfg.reconstruction.neighborRadiusScale, errorText)
|
||||||
|
|| !parseIntParam(parsed, "runEveryNthFrame", cfg.reconstruction.runEveryNthFrame, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (stageId == "pcl_greedy_triangulation") {
|
||||||
|
if (!parseFloatParam(parsed, "searchRadius", cfg.reconstruction.pclGreedySearchRadius, errorText)
|
||||||
|
|| !parseFloatParam(parsed, "mu", cfg.reconstruction.pclGreedyMu, errorText)
|
||||||
|
|| !parseIntParam(parsed, "maxNearest", cfg.reconstruction.pclGreedyMaxNearest, errorText)
|
||||||
|
|| !parseFloatParam(parsed, "maxSurfaceAngle", cfg.reconstruction.pclGreedyMaxSurfaceAngle, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (stageId == "pcl_poisson_reconstruction") {
|
||||||
|
if (!parseIntParam(parsed, "poissonDepth", cfg.reconstruction.pclPoissonDepth, errorText)
|
||||||
|
|| !parseFloatParam(parsed, "samplesPerNode", cfg.reconstruction.pclPoissonSamplesPerNode, errorText)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} // namespace core
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#ifndef PIPELINE_STAGE_DEFAULTS_H
|
||||||
|
#define PIPELINE_STAGE_DEFAULTS_H
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include "pipeline_config.h"
|
||||||
|
|
||||||
|
namespace core
|
||||||
|
{
|
||||||
|
bool applyStageDefaultsToConfig(
|
||||||
|
const QString &stageId,
|
||||||
|
const QString &defaultsText,
|
||||||
|
PipelineConfig &cfg,
|
||||||
|
QString &errorText);
|
||||||
|
} // namespace core
|
||||||
|
|
||||||
|
#endif // PIPELINE_STAGE_DEFAULTS_H
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
#include "tuning_service.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
#include "pipeline_config_validation.h"
|
||||||
|
#include "../factories/pipeline/desktop_pipeline_factory.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
QVector<float> withFallback(const QVector<float> &values, const float fallback)
|
||||||
|
{
|
||||||
|
if (!values.isEmpty()) {
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
return QVector<float>{fallback};
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace core
|
||||||
|
{
|
||||||
|
AutotuneResult runGridAutotune(
|
||||||
|
const PointCloudFrame &frame,
|
||||||
|
const PipelineConfig &baseConfig,
|
||||||
|
const AutotuneSearchSpace &searchSpace,
|
||||||
|
const HoleAwareScoreWeights &weights,
|
||||||
|
const AutotuneLimits &limits)
|
||||||
|
{
|
||||||
|
AutotuneResult out;
|
||||||
|
if (frame.points.isEmpty()) {
|
||||||
|
out.errorText = "Autotune input frame is empty.";
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QVector<float> leafValues = withFallback(searchSpace.pclVoxelLeafSizes, baseConfig.preprocess.pclVoxelLeafSize);
|
||||||
|
const QVector<float> greedyValues = withFallback(searchSpace.pclGreedySearchRadii, baseConfig.reconstruction.pclGreedySearchRadius);
|
||||||
|
const QVector<float> neighborValues = withFallback(searchSpace.neighborRadiusScales, baseConfig.reconstruction.neighborRadiusScale);
|
||||||
|
|
||||||
|
const int requestedTopK = qMax(1, limits.topK);
|
||||||
|
const int maxCandidates = limits.maxCandidates > 0 ? limits.maxCandidates : std::numeric_limits<int>::max();
|
||||||
|
|
||||||
|
bool hasBest = false;
|
||||||
|
int evaluated = 0;
|
||||||
|
|
||||||
|
for (const float leaf : leafValues) {
|
||||||
|
for (const float greedyRadius : greedyValues) {
|
||||||
|
for (const float neighborRadius : neighborValues) {
|
||||||
|
if (evaluated >= maxCandidates) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
PipelineConfig cfg = baseConfig;
|
||||||
|
cfg.preprocess.pclVoxelLeafSize = leaf;
|
||||||
|
cfg.reconstruction.pclGreedySearchRadius = greedyRadius;
|
||||||
|
cfg.reconstruction.neighborRadiusScale = neighborRadius;
|
||||||
|
|
||||||
|
QString errorText;
|
||||||
|
if (!normalizeAndValidatePipelineConfig(cfg, errorText)) {
|
||||||
|
out.errorText = QString("Autotune candidate rejected: %1").arg(errorText);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PipelineExecutor executor = factories::pipeline::createDesktopPipelineExecutor(cfg);
|
||||||
|
const PipelineResult result = executor.run(frame);
|
||||||
|
const HoleAwareScoreBreakdown breakdown = computeHoleAwareScore(result, weights);
|
||||||
|
|
||||||
|
AutotuneCandidateResult candidate;
|
||||||
|
candidate.config = cfg;
|
||||||
|
candidate.score = breakdown.totalScore;
|
||||||
|
candidate.breakdown = breakdown;
|
||||||
|
candidate.inputPoints = result.stats.inputPoints;
|
||||||
|
candidate.afterPreprocessPoints = result.stats.afterPreprocessingPoints;
|
||||||
|
candidate.outputTriangles = result.stats.outputTriangles;
|
||||||
|
candidate.reconstructionMs = result.stats.reconstructionMs;
|
||||||
|
|
||||||
|
out.topCandidates.push_back(candidate);
|
||||||
|
if (!hasBest || candidate.score > out.best.score) {
|
||||||
|
out.best = candidate;
|
||||||
|
hasBest = true;
|
||||||
|
}
|
||||||
|
++evaluated;
|
||||||
|
}
|
||||||
|
if (evaluated >= maxCandidates) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (evaluated >= maxCandidates) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(
|
||||||
|
out.topCandidates.begin(),
|
||||||
|
out.topCandidates.end(),
|
||||||
|
[](const AutotuneCandidateResult &a, const AutotuneCandidateResult &b) {
|
||||||
|
return a.score > b.score;
|
||||||
|
});
|
||||||
|
if (out.topCandidates.size() > requestedTopK) {
|
||||||
|
out.topCandidates.resize(requestedTopK);
|
||||||
|
}
|
||||||
|
|
||||||
|
out.evaluatedCandidates = evaluated;
|
||||||
|
out.ok = hasBest;
|
||||||
|
if (!out.ok) {
|
||||||
|
out.errorText = "No candidates evaluated during autotune.";
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
} // namespace core
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
#ifndef TUNING_SERVICE_H
|
||||||
|
#define TUNING_SERVICE_H
|
||||||
|
|
||||||
|
#include <QVector>
|
||||||
|
|
||||||
|
#include "hole_aware_score.h"
|
||||||
|
#include "pipeline_config.h"
|
||||||
|
#include "point_cloud_types.h"
|
||||||
|
|
||||||
|
namespace core
|
||||||
|
{
|
||||||
|
struct AutotuneSearchSpace
|
||||||
|
{
|
||||||
|
QVector<float> pclVoxelLeafSizes;
|
||||||
|
QVector<float> pclGreedySearchRadii;
|
||||||
|
QVector<float> neighborRadiusScales;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AutotuneLimits
|
||||||
|
{
|
||||||
|
int maxCandidates = 0;
|
||||||
|
int topK = 5;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AutotuneCandidateResult
|
||||||
|
{
|
||||||
|
PipelineConfig config;
|
||||||
|
double score = 0.0;
|
||||||
|
HoleAwareScoreBreakdown breakdown;
|
||||||
|
int inputPoints = 0;
|
||||||
|
int afterPreprocessPoints = 0;
|
||||||
|
int outputTriangles = 0;
|
||||||
|
qint64 reconstructionMs = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AutotuneResult
|
||||||
|
{
|
||||||
|
bool ok = false;
|
||||||
|
QString errorText;
|
||||||
|
int evaluatedCandidates = 0;
|
||||||
|
AutotuneCandidateResult best;
|
||||||
|
QVector<AutotuneCandidateResult> topCandidates;
|
||||||
|
};
|
||||||
|
|
||||||
|
AutotuneResult runGridAutotune(
|
||||||
|
const PointCloudFrame &frame,
|
||||||
|
const PipelineConfig &baseConfig,
|
||||||
|
const AutotuneSearchSpace &searchSpace,
|
||||||
|
const HoleAwareScoreWeights &weights,
|
||||||
|
const AutotuneLimits &limits);
|
||||||
|
} // namespace core
|
||||||
|
|
||||||
|
#endif // TUNING_SERVICE_H
|
||||||
@@ -1,9 +1,18 @@
|
|||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include "cli/pipeline_cli_runner.h"
|
||||||
#include "ui/mainwindow.h"
|
#include "ui/mainwindow.h"
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
|
for (int i = 1; i < argc; ++i) {
|
||||||
|
if (QString::fromLocal8Bit(argv[i]) == "--cli") {
|
||||||
|
return runCliPipeline(argc, argv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QCoreApplication::setAttribute(Qt::AA_UseSoftwareOpenGL);
|
QCoreApplication::setAttribute(Qt::AA_UseSoftwareOpenGL);
|
||||||
QApplication app(argc, argv);
|
QApplication app(argc, argv);
|
||||||
MainWindow window;
|
MainWindow window;
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
#include "pipeline_smoke_tests.h"
|
#include "pipeline_smoke_tests.h"
|
||||||
|
|
||||||
|
#include <QtMath>
|
||||||
|
|
||||||
#include "../algorithms/reconstruction/surface_reconstruction.h"
|
#include "../algorithms/reconstruction/surface_reconstruction.h"
|
||||||
#include "../adapters/sources/ros2_point_cloud_source.h"
|
#include "../adapters/sources/ros2_point_cloud_source.h"
|
||||||
#include "../core/pipeline_config.h"
|
#include "../core/pipeline_config.h"
|
||||||
|
#include "../core/pipeline_config_validation.h"
|
||||||
#include "../core/pipeline_executor.h"
|
#include "../core/pipeline_executor.h"
|
||||||
|
#include "../core/hole_aware_score.h"
|
||||||
|
#include "../core/pipeline_stage_defaults.h"
|
||||||
|
#include "../core/tuning_service.h"
|
||||||
#include "../factories/pipeline/desktop_pipeline_factory.h"
|
#include "../factories/pipeline/desktop_pipeline_factory.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
@@ -168,6 +174,152 @@ bool runReconstructionPluginSwitchCase(QString &failureReason)
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool runStageDefaultsApplyCase(QString &failureReason)
|
||||||
|
{
|
||||||
|
core::PipelineConfig config = core::makeDesktopDebugConfig();
|
||||||
|
QString errorText;
|
||||||
|
const bool ok = core::applyStageDefaultsToConfig(
|
||||||
|
"pcl_voxel_grid",
|
||||||
|
"leaf=0.06",
|
||||||
|
config,
|
||||||
|
errorText);
|
||||||
|
if (!ok) {
|
||||||
|
failureReason = QString("Stage defaults apply failed unexpectedly: %1").arg(errorText);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (qAbs(config.preprocess.pclVoxelLeafSize - 0.06f) > 0.0001f) {
|
||||||
|
failureReason = "pclVoxelLeafSize was not updated by backend defaults parser.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool runStageDefaultsValidationCase(QString &failureReason)
|
||||||
|
{
|
||||||
|
core::PipelineConfig config = core::makeDesktopDebugConfig();
|
||||||
|
QString errorText;
|
||||||
|
const bool ok = core::applyStageDefaultsToConfig(
|
||||||
|
"pcl_radius_outlier",
|
||||||
|
"radius=bad,minNeighbors=8",
|
||||||
|
config,
|
||||||
|
errorText);
|
||||||
|
if (ok) {
|
||||||
|
failureReason = "Invalid defaults were accepted unexpectedly.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!errorText.contains("Параметр 'radius'")) {
|
||||||
|
failureReason = QString("Unexpected validation message: %1").arg(errorText);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool runPipelineConfigValidationCase(QString &failureReason)
|
||||||
|
{
|
||||||
|
core::PipelineConfig config = core::makeDesktopDebugConfig();
|
||||||
|
config.preprocessPlugins = QStringList() << "pcl_voxel_grid" << "pcl_voxel_grid" << "pcl_statistical_outlier";
|
||||||
|
config.reconstructionPlugin = "pcl_greedy_triangulation";
|
||||||
|
QString errorText;
|
||||||
|
if (!core::normalizeAndValidatePipelineConfig(config, errorText)) {
|
||||||
|
failureReason = QString("Config validation failed unexpectedly: %1").arg(errorText);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (config.preprocessPlugins.size() != 2) {
|
||||||
|
failureReason = "Config validation did not de-duplicate preprocess plugins.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool runHoleAwareScoreCase(QString &failureReason)
|
||||||
|
{
|
||||||
|
core::PipelineResult result;
|
||||||
|
result.stats.inputPoints = 100;
|
||||||
|
result.stats.afterPreprocessingPoints = 90;
|
||||||
|
result.stats.outputTriangles = 120;
|
||||||
|
result.stats.reconstructionMs = 20;
|
||||||
|
const core::HoleAwareScoreBreakdown score = core::computeHoleAwareScore(
|
||||||
|
result,
|
||||||
|
core::HoleAwareScoreWeights());
|
||||||
|
if (score.totalScore <= 0.0) {
|
||||||
|
failureReason = "Hole-aware score should be positive for a dense reconstructed surface.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (score.connectivityScore <= 0.0 || score.coverageScore <= 0.0) {
|
||||||
|
failureReason = "Hole-aware score breakdown has invalid non-positive components.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool runGridAutotuneCase(QString &failureReason)
|
||||||
|
{
|
||||||
|
core::PointCloudFrame frame;
|
||||||
|
frame.points = buildDemoCloud();
|
||||||
|
frame.sourceLabel = "smoke-autotune";
|
||||||
|
|
||||||
|
core::PipelineConfig config = core::makeDesktopDebugConfig();
|
||||||
|
config.preprocessPlugins = QStringList() << "pcl_voxel_grid" << "pcl_statistical_outlier";
|
||||||
|
config.reconstructionPlugin = "pcl_greedy_triangulation";
|
||||||
|
|
||||||
|
core::AutotuneSearchSpace searchSpace;
|
||||||
|
searchSpace.pclVoxelLeafSizes = QVector<float>{0.02f, 0.05f};
|
||||||
|
searchSpace.pclGreedySearchRadii = QVector<float>{0.06f, 0.10f};
|
||||||
|
searchSpace.neighborRadiusScales = QVector<float>{2.5f};
|
||||||
|
|
||||||
|
core::AutotuneLimits limits;
|
||||||
|
limits.maxCandidates = 10;
|
||||||
|
limits.topK = 3;
|
||||||
|
|
||||||
|
const core::AutotuneResult result = core::runGridAutotune(
|
||||||
|
frame,
|
||||||
|
config,
|
||||||
|
searchSpace,
|
||||||
|
core::HoleAwareScoreWeights(),
|
||||||
|
limits);
|
||||||
|
if (!result.ok) {
|
||||||
|
failureReason = QString("Autotune failed unexpectedly: %1").arg(result.errorText);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (result.evaluatedCandidates != 4) {
|
||||||
|
failureReason = QString("Autotune evaluated %1 candidates instead of 4.").arg(result.evaluatedCandidates);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (result.topCandidates.isEmpty()) {
|
||||||
|
failureReason = "Autotune topCandidates is empty.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (result.best.score < result.topCandidates.first().score) {
|
||||||
|
failureReason = "Autotune best candidate is not the top-scoring candidate.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool runGridAutotuneValidationCase(QString &failureReason)
|
||||||
|
{
|
||||||
|
core::PointCloudFrame emptyFrame;
|
||||||
|
core::PipelineConfig config = core::makeDesktopDebugConfig();
|
||||||
|
core::AutotuneSearchSpace searchSpace;
|
||||||
|
searchSpace.pclVoxelLeafSizes = QVector<float>{0.02f};
|
||||||
|
searchSpace.pclGreedySearchRadii = QVector<float>{0.08f};
|
||||||
|
const core::AutotuneResult result = core::runGridAutotune(
|
||||||
|
emptyFrame,
|
||||||
|
config,
|
||||||
|
searchSpace,
|
||||||
|
core::HoleAwareScoreWeights(),
|
||||||
|
core::AutotuneLimits());
|
||||||
|
if (result.ok) {
|
||||||
|
failureReason = "Autotune accepted empty input frame unexpectedly.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!result.errorText.contains("empty")) {
|
||||||
|
failureReason = QString("Unexpected autotune validation message: %1").arg(result.errorText);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
namespace tests
|
namespace tests
|
||||||
@@ -195,6 +347,24 @@ bool runPipelineSmokeTests(QString &failureReason)
|
|||||||
if (!runReconstructionPluginSwitchCase(failureReason)) {
|
if (!runReconstructionPluginSwitchCase(failureReason)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (!runStageDefaultsApplyCase(failureReason)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!runStageDefaultsValidationCase(failureReason)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!runPipelineConfigValidationCase(failureReason)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!runHoleAwareScoreCase(failureReason)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!runGridAutotuneCase(failureReason)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!runGridAutotuneValidationCase(failureReason)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} // namespace tests
|
} // namespace tests
|
||||||
|
|||||||
+4
-399
@@ -29,6 +29,8 @@
|
|||||||
#include <QtMath>
|
#include <QtMath>
|
||||||
|
|
||||||
#include "../adapters/sources/file_point_cloud_source.h"
|
#include "../adapters/sources/file_point_cloud_source.h"
|
||||||
|
#include "../core/pipeline_config_validation.h"
|
||||||
|
#include "../core/pipeline_stage_defaults.h"
|
||||||
#include "../factories/pipeline/desktop_pipeline_factory.h"
|
#include "../factories/pipeline/desktop_pipeline_factory.h"
|
||||||
#include "glview.h"
|
#include "glview.h"
|
||||||
|
|
||||||
@@ -159,336 +161,6 @@ const StageMeta *findMeta(const QString &id)
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
QMap<QString, QString> parseDefaultsMap(const QString &defaultsText)
|
|
||||||
{
|
|
||||||
QMap<QString, QString> parsed;
|
|
||||||
const QStringList chunks = defaultsText.split(',', Qt::SkipEmptyParts);
|
|
||||||
for (const QString &chunkRaw : chunks) {
|
|
||||||
const QString chunk = chunkRaw.trimmed();
|
|
||||||
if (chunk.isEmpty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const int pos = chunk.indexOf('=');
|
|
||||||
if (pos <= 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const QString key = chunk.left(pos).trimmed();
|
|
||||||
const QString value = chunk.mid(pos + 1).trimmed();
|
|
||||||
if (!key.isEmpty()) {
|
|
||||||
parsed.insert(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return parsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool parseIntParam(const QMap<QString, QString> &parsed, const QString &key, int &target, QString &errorText)
|
|
||||||
{
|
|
||||||
if (!parsed.contains(key)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
bool ok = false;
|
|
||||||
const int value = parsed.value(key).toInt(&ok);
|
|
||||||
if (!ok) {
|
|
||||||
errorText = QString("Параметр '%1' должен быть целым числом.").arg(key);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
target = value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool parseFloatParam(const QMap<QString, QString> &parsed, const QString &key, float &target, QString &errorText)
|
|
||||||
{
|
|
||||||
if (!parsed.contains(key)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
bool ok = false;
|
|
||||||
const float value = parsed.value(key).toFloat(&ok);
|
|
||||||
if (!ok) {
|
|
||||||
errorText = QString("Параметр '%1' должен быть числом.").arg(key);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
target = value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool parseDoubleParam(const QMap<QString, QString> &parsed, const QString &key, double &target, QString &errorText)
|
|
||||||
{
|
|
||||||
if (!parsed.contains(key)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
bool ok = false;
|
|
||||||
const double value = parsed.value(key).toDouble(&ok);
|
|
||||||
if (!ok) {
|
|
||||||
errorText = QString("Параметр '%1' должен быть числом.").arg(key);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
target = value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool applyStageDefaultsToConfig(
|
|
||||||
const QString &stageId,
|
|
||||||
const QString &defaultsText,
|
|
||||||
core::PipelineConfig &cfg,
|
|
||||||
QString &errorText)
|
|
||||||
{
|
|
||||||
const QMap<QString, QString> parsed = parseDefaultsMap(defaultsText);
|
|
||||||
if (parsed.isEmpty()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stageId == "keep_largest_cluster") {
|
|
||||||
return parseDoubleParam(parsed, "clusterJoinDistanceScale", cfg.preprocess.clusterJoinDistanceScale, errorText);
|
|
||||||
}
|
|
||||||
if (stageId == "downsample_dense") {
|
|
||||||
return parseDoubleParam(parsed, "downsampleCellScale", cfg.preprocess.downsampleCellScale, errorText);
|
|
||||||
}
|
|
||||||
if (stageId == "pcl_voxel_grid") {
|
|
||||||
float leaf = cfg.preprocess.pclVoxelLeafSize;
|
|
||||||
if (!parseFloatParam(parsed, "leaf", leaf, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!parseFloatParam(parsed, "pclVoxelLeafSize", leaf, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
cfg.preprocess.pclVoxelLeafSize = leaf;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (stageId == "pcl_statistical_outlier") {
|
|
||||||
if (!parseIntParam(parsed, "meanK", cfg.preprocess.pclSorMeanK, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!parseDoubleParam(parsed, "stddev", cfg.preprocess.pclSorStdDevMul, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (stageId == "pcl_radius_outlier") {
|
|
||||||
if (!parseFloatParam(parsed, "radius", cfg.preprocess.pclRorRadius, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!parseIntParam(parsed, "minNeighbors", cfg.preprocess.pclRorMinNeighbors, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (stageId == "pcl_model_outlier") {
|
|
||||||
if (!parseFloatParam(parsed, "threshold", cfg.preprocess.pclMorThreshold, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!parseFloatParam(parsed, "pclMorThreshold", cfg.preprocess.pclMorThreshold, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (stageId == "pcl_shadow_points") {
|
|
||||||
if (!parseFloatParam(parsed, "shadowThreshold", cfg.preprocess.pclShadowThreshold, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!parseFloatParam(parsed, "pclShadowThreshold", cfg.preprocess.pclShadowThreshold, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (stageId == "pcl_approximate_voxel_grid") {
|
|
||||||
if (!parseFloatParam(parsed, "leaf", cfg.preprocess.pclApproxLeafSize, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!parseFloatParam(parsed, "pclApproxLeafSize", cfg.preprocess.pclApproxLeafSize, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (stageId == "pcl_voxel_grid_label") {
|
|
||||||
if (!parseFloatParam(parsed, "leaf", cfg.preprocess.pclLabelLeafSize, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!parseFloatParam(parsed, "pclLabelLeafSize", cfg.preprocess.pclLabelLeafSize, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (stageId == "pcl_voxel_grid_covariance") {
|
|
||||||
if (!parseFloatParam(parsed, "leaf", cfg.preprocess.pclCovLeafSize, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!parseFloatParam(parsed, "pclCovLeafSize", cfg.preprocess.pclCovLeafSize, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (stageId == "pcl_grid_minimum") {
|
|
||||||
if (!parseFloatParam(parsed, "resolution", cfg.preprocess.pclGridMinimumResolution, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!parseFloatParam(parsed, "pclGridMinimumResolution", cfg.preprocess.pclGridMinimumResolution, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (stageId == "pcl_farthest_point_sampling") {
|
|
||||||
if (!parseIntParam(parsed, "sample", cfg.preprocess.pclFpsSampleCount, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!parseIntParam(parsed, "pclFpsSampleCount", cfg.preprocess.pclFpsSampleCount, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (stageId == "pcl_normal_space_sampling") {
|
|
||||||
if (!parseIntParam(parsed, "sample", cfg.preprocess.pclNormalSpaceSampleCount, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!parseIntParam(parsed, "pclNormalSpaceSampleCount", cfg.preprocess.pclNormalSpaceSampleCount, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (stageId == "pcl_sampling_surface_normal") {
|
|
||||||
if (!parseIntParam(parsed, "sample", cfg.preprocess.pclSurfaceNormalSampleCount, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!parseIntParam(parsed, "pclSurfaceNormalSampleCount", cfg.preprocess.pclSurfaceNormalSampleCount, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (stageId == "pcl_pass_through") {
|
|
||||||
if (parsed.contains("axis")) {
|
|
||||||
const QString axis = parsed.value("axis").toLower();
|
|
||||||
if (axis != "x" && axis != "y" && axis != "z") {
|
|
||||||
errorText = "Параметр 'axis' должен быть x, y или z.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
cfg.preprocess.pclPassAxis = axis;
|
|
||||||
}
|
|
||||||
if (!parseFloatParam(parsed, "min", cfg.preprocess.pclPassMin, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!parseFloatParam(parsed, "max", cfg.preprocess.pclPassMax, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (stageId == "pcl_crop_box" || stageId == "pcl_crop_hull") {
|
|
||||||
if (!parseFloatParam(parsed, "minX", cfg.preprocess.pclCropBoxMinX, errorText)
|
|
||||||
|| !parseFloatParam(parsed, "minY", cfg.preprocess.pclCropBoxMinY, errorText)
|
|
||||||
|| !parseFloatParam(parsed, "minZ", cfg.preprocess.pclCropBoxMinZ, errorText)
|
|
||||||
|| !parseFloatParam(parsed, "maxX", cfg.preprocess.pclCropBoxMaxX, errorText)
|
|
||||||
|| !parseFloatParam(parsed, "maxY", cfg.preprocess.pclCropBoxMaxY, errorText)
|
|
||||||
|| !parseFloatParam(parsed, "maxZ", cfg.preprocess.pclCropBoxMaxZ, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (stageId == "pcl_frustum_culling") {
|
|
||||||
if (!parseFloatParam(parsed, "near", cfg.preprocess.pclFrustumNear, errorText)
|
|
||||||
|| !parseFloatParam(parsed, "far", cfg.preprocess.pclFrustumFar, errorText)
|
|
||||||
|| !parseFloatParam(parsed, "hfov", cfg.preprocess.pclFrustumHfovDeg, errorText)
|
|
||||||
|| !parseFloatParam(parsed, "vfov", cfg.preprocess.pclFrustumVfovDeg, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (stageId == "pcl_plane_clipper_3d") {
|
|
||||||
if (!parseFloatParam(parsed, "a", cfg.preprocess.pclClipPlaneA, errorText)
|
|
||||||
|| !parseFloatParam(parsed, "b", cfg.preprocess.pclClipPlaneB, errorText)
|
|
||||||
|| !parseFloatParam(parsed, "c", cfg.preprocess.pclClipPlaneC, errorText)
|
|
||||||
|| !parseFloatParam(parsed, "d", cfg.preprocess.pclClipPlaneD, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (parsed.contains("keepPositive")) {
|
|
||||||
const QString v = parsed.value("keepPositive").trimmed().toLower();
|
|
||||||
if (v != "true" && v != "false") {
|
|
||||||
errorText = "Параметр 'keepPositive' должен быть true или false.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
cfg.preprocess.pclClipKeepPositive = (v == "true");
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (stageId == "pcl_conditional_removal") {
|
|
||||||
if (!parseFloatParam(parsed, "zMin", cfg.preprocess.pclConditionalZMin, errorText)
|
|
||||||
|| !parseFloatParam(parsed, "zMax", cfg.preprocess.pclConditionalZMax, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (stageId == "pcl_extract_indices") {
|
|
||||||
if (!parseIntParam(parsed, "nth", cfg.preprocess.pclExtractNth, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (stageId == "pcl_functor_filter") {
|
|
||||||
if (!parseFloatParam(parsed, "radiusMax", cfg.preprocess.pclFunctorRadiusMax, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (stageId == "pcl_project_inliers") {
|
|
||||||
if (!parseFloatParam(parsed, "a", cfg.preprocess.pclProjectPlaneA, errorText)
|
|
||||||
|| !parseFloatParam(parsed, "b", cfg.preprocess.pclProjectPlaneB, errorText)
|
|
||||||
|| !parseFloatParam(parsed, "c", cfg.preprocess.pclProjectPlaneC, errorText)
|
|
||||||
|| !parseFloatParam(parsed, "d", cfg.preprocess.pclProjectPlaneD, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (stageId == "pcl_normal_refinement") {
|
|
||||||
if (!parseFloatParam(parsed, "radius", cfg.preprocess.pclNormalRefineRadius, errorText)
|
|
||||||
|| !parseIntParam(parsed, "iterations", cfg.preprocess.pclNormalRefineIterations, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (stageId == "pcl_bilateral_filter" || stageId == "pcl_fast_bilateral_filter" || stageId == "pcl_fast_bilateral_filter_omp") {
|
|
||||||
if (!parseFloatParam(parsed, "sigmaS", cfg.preprocess.pclBilateralSigmaS, errorText)
|
|
||||||
|| !parseFloatParam(parsed, "sigmaR", cfg.preprocess.pclBilateralSigmaR, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (stageId == "pcl_convolution" || stageId == "pcl_gaussian_kernel" || stageId == "pcl_gaussian_kernel_rgb") {
|
|
||||||
if (!parseFloatParam(parsed, "sigma", cfg.preprocess.pclConvolutionKernelSigma, errorText)
|
|
||||||
|| !parseIntParam(parsed, "kernel", cfg.preprocess.pclConvolutionKernelSize, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (stageId == "pcl_voxel_grid_occlusion") {
|
|
||||||
if (!parseFloatParam(parsed, "leaf", cfg.preprocess.pclVoxelOccLeaf, errorText)
|
|
||||||
|| !parseIntParam(parsed, "minHits", cfg.preprocess.pclVoxelOccMinHits, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (stageId == "surface_fallback") {
|
|
||||||
if (!parseFloatParam(parsed, "neighborRadiusScale", cfg.reconstruction.neighborRadiusScale, errorText)
|
|
||||||
|| !parseIntParam(parsed, "runEveryNthFrame", cfg.reconstruction.runEveryNthFrame, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (stageId == "pcl_greedy_triangulation") {
|
|
||||||
if (!parseFloatParam(parsed, "searchRadius", cfg.reconstruction.pclGreedySearchRadius, errorText)
|
|
||||||
|| !parseFloatParam(parsed, "mu", cfg.reconstruction.pclGreedyMu, errorText)
|
|
||||||
|| !parseIntParam(parsed, "maxNearest", cfg.reconstruction.pclGreedyMaxNearest, errorText)
|
|
||||||
|| !parseFloatParam(parsed, "maxSurfaceAngle", cfg.reconstruction.pclGreedyMaxSurfaceAngle, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (stageId == "pcl_poisson_reconstruction") {
|
|
||||||
if (!parseIntParam(parsed, "poissonDepth", cfg.reconstruction.pclPoissonDepth, errorText)
|
|
||||||
|| !parseFloatParam(parsed, "samplesPerNode", cfg.reconstruction.pclPoissonSamplesPerNode, errorText)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
MainWindow::MainWindow(QWidget *parent)
|
MainWindow::MainWindow(QWidget *parent)
|
||||||
@@ -629,7 +301,7 @@ void MainWindow::rebuildExecutorFromConfig()
|
|||||||
if (card.family == "reconstruction") {
|
if (card.family == "reconstruction") {
|
||||||
cfg.reconstructionPlugin = card.id;
|
cfg.reconstructionPlugin = card.id;
|
||||||
}
|
}
|
||||||
if (!applyStageDefaultsToConfig(card.id, card.defaults, cfg, errorText)) {
|
if (!core::applyStageDefaultsToConfig(card.id, card.defaults, cfg, errorText)) {
|
||||||
QMessageBox::warning(this, "Параметры фильтра", errorText);
|
QMessageBox::warning(this, "Параметры фильтра", errorText);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1468,74 +1140,7 @@ void MainWindow::loadSnapshot(const QString &slotName)
|
|||||||
|
|
||||||
bool MainWindow::normalizeAndValidateConfig(core::PipelineConfig &config, QString &errorText) const
|
bool MainWindow::normalizeAndValidateConfig(core::PipelineConfig &config, QString &errorText) const
|
||||||
{
|
{
|
||||||
QSet<QString> seen;
|
return core::normalizeAndValidatePipelineConfig(config, errorText);
|
||||||
QStringList normalized;
|
|
||||||
const QStringList allowed = QStringList()
|
|
||||||
<< "keep_largest_cluster"
|
|
||||||
<< "pcl_remove_nan"
|
|
||||||
<< "pcl_remove_nan_normals"
|
|
||||||
<< "pcl_pass_through"
|
|
||||||
<< "pcl_crop_box"
|
|
||||||
<< "pcl_crop_hull"
|
|
||||||
<< "pcl_frustum_culling"
|
|
||||||
<< "pcl_plane_clipper_3d"
|
|
||||||
<< "pcl_conditional_removal"
|
|
||||||
<< "pcl_extract_indices"
|
|
||||||
<< "pcl_functor_filter"
|
|
||||||
<< "pcl_project_inliers"
|
|
||||||
<< "pcl_normal_refinement"
|
|
||||||
<< "pcl_bilateral_filter"
|
|
||||||
<< "pcl_fast_bilateral_filter"
|
|
||||||
<< "pcl_fast_bilateral_filter_omp"
|
|
||||||
<< "pcl_convolution"
|
|
||||||
<< "pcl_gaussian_kernel"
|
|
||||||
<< "pcl_gaussian_kernel_rgb"
|
|
||||||
<< "pcl_voxel_grid_occlusion"
|
|
||||||
<< "downsample_dense"
|
|
||||||
<< "pcl_voxel_grid"
|
|
||||||
<< "pcl_statistical_outlier"
|
|
||||||
<< "pcl_radius_outlier"
|
|
||||||
<< "pcl_model_outlier"
|
|
||||||
<< "pcl_shadow_points"
|
|
||||||
<< "pcl_approximate_voxel_grid"
|
|
||||||
<< "pcl_voxel_grid_label"
|
|
||||||
<< "pcl_voxel_grid_covariance"
|
|
||||||
<< "pcl_grid_minimum"
|
|
||||||
<< "pcl_farthest_point_sampling"
|
|
||||||
<< "pcl_normal_space_sampling"
|
|
||||||
<< "pcl_sampling_surface_normal";
|
|
||||||
for (const QString &id : config.preprocessPlugins) {
|
|
||||||
if (id.isEmpty() || seen.contains(id)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!allowed.contains(id)) {
|
|
||||||
errorText = QString("Unknown preprocess plugin: %1").arg(id);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
seen.insert(id);
|
|
||||||
normalized.push_back(id);
|
|
||||||
}
|
|
||||||
if (normalized.isEmpty()) {
|
|
||||||
normalized = QStringList() << "keep_largest_cluster" << "downsample_dense";
|
|
||||||
}
|
|
||||||
config.preprocessPlugins = normalized;
|
|
||||||
|
|
||||||
const QStringList reconAllowed = QStringList() << "surface_fallback" << "pcl_greedy_triangulation" << "pcl_poisson_reconstruction";
|
|
||||||
if (!reconAllowed.contains(config.reconstructionPlugin)) {
|
|
||||||
errorText = QString("Unknown reconstruction plugin: %1").arg(config.reconstructionPlugin);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
#ifndef PCL_ENABLED
|
|
||||||
for (int i = 0; i < config.preprocessPlugins.size(); ++i) {
|
|
||||||
if (config.preprocessPlugins[i].startsWith("pcl_")) {
|
|
||||||
config.preprocessPlugins[i] = "downsample_dense";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (config.reconstructionPlugin.startsWith("pcl_")) {
|
|
||||||
config.reconstructionPlugin = "surface_fallback";
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QString MainWindow::pipelineSummary() const
|
QString MainWindow::pipelineSummary() const
|
||||||
|
|||||||
Reference in New Issue
Block a user