Добавил конструктор фильтров
This commit is contained in:
+79
-5
@@ -1,17 +1,91 @@
|
||||
QT += core gui widgets opengl
|
||||
QT += core gui widgets opengl quick quickwidgets qml
|
||||
|
||||
CONFIG += c++11
|
||||
CONFIG += c++14
|
||||
TEMPLATE = app
|
||||
TARGET = DotsToSirface
|
||||
LIBS += -lopengl32
|
||||
|
||||
win32:LIBS += -lopengl32
|
||||
unix:LIBS += -lGL
|
||||
|
||||
contains(DEFINES, PCL_ENABLED) {
|
||||
message(PCL support enabled)
|
||||
win32 {
|
||||
isEmpty(PCL_ROOT) {
|
||||
PCL_ROOT = C:/PCL
|
||||
}
|
||||
INCLUDEPATH += $$PCL_ROOT/include $$PCL_ROOT/include/pcl-1.12
|
||||
LIBS += -L$$PCL_ROOT/lib \
|
||||
-lpcl_common \
|
||||
-lpcl_io \
|
||||
-lpcl_filters \
|
||||
-lpcl_features \
|
||||
-lpcl_kdtree \
|
||||
-lpcl_search \
|
||||
-lpcl_surface
|
||||
}
|
||||
|
||||
unix {
|
||||
CONFIG += link_pkgconfig
|
||||
PKGCONFIG += \
|
||||
pcl_common \
|
||||
pcl_io \
|
||||
pcl_filters \
|
||||
pcl_features \
|
||||
pcl_kdtree \
|
||||
pcl_search \
|
||||
pcl_surface
|
||||
}
|
||||
}
|
||||
|
||||
SOURCES += \
|
||||
src/main.cpp \
|
||||
src/geometry/surface_reconstruction.cpp \
|
||||
src/algorithms/preprocess/preprocess_algorithms.cpp \
|
||||
src/algorithms/reconstruction/surface_reconstruction.cpp \
|
||||
src/adapters/reconstruction/reconstruction_adapter.cpp \
|
||||
src/adapters/pcl/pcl_point_cloud_adapter.cpp \
|
||||
src/adapters/registration/registration_adapter.cpp \
|
||||
src/adapters/ros2/pointcloud2_adapter.cpp \
|
||||
src/adapters/ros2/tf2_adapter.cpp \
|
||||
src/adapters/sources/file_point_cloud_source.cpp \
|
||||
src/adapters/sources/ros2_point_cloud_source.cpp \
|
||||
src/core/pipeline_config.cpp \
|
||||
src/core/pipeline_executor.cpp \
|
||||
src/core/pipeline_plugin_registry.cpp \
|
||||
src/factories/pipeline/desktop_pipeline_factory.cpp \
|
||||
src/strategies/registration_icp_stage.cpp \
|
||||
src/strategies/preprocess_basic_stages.cpp \
|
||||
src/strategies/preprocess_pcl_stages.cpp \
|
||||
src/strategies/transform_tf_stage.cpp \
|
||||
src/tests/pipeline_smoke_tests.cpp \
|
||||
src/ui/mainwindow.cpp \
|
||||
src/ui/glview.cpp
|
||||
|
||||
HEADERS += \
|
||||
src/geometry/surface_reconstruction.h \
|
||||
src/algorithms/preprocess/preprocess_algorithms.h \
|
||||
src/algorithms/reconstruction/surface_reconstruction.h \
|
||||
src/adapters/reconstruction/reconstruction_adapter.h \
|
||||
src/adapters/pcl/pcl_point_cloud_adapter.h \
|
||||
src/adapters/registration/registration_adapter.h \
|
||||
src/adapters/ros2/pointcloud2_adapter.h \
|
||||
src/adapters/ros2/tf2_adapter.h \
|
||||
src/adapters/sources/file_point_cloud_source.h \
|
||||
src/adapters/sources/ros2_point_cloud_source.h \
|
||||
src/core/data_source.h \
|
||||
src/core/point_cloud_types.h \
|
||||
src/core/pipeline_config.h \
|
||||
src/core/pipeline_stage.h \
|
||||
src/core/pipeline_executor.h \
|
||||
src/core/pipeline_plugin_registry.h \
|
||||
src/factories/pipeline/desktop_pipeline_factory.h \
|
||||
src/strategies/preprocess_basic_stages.h \
|
||||
src/strategies/preprocess_pcl_stages.h \
|
||||
src/strategies/registration_icp_stage.h \
|
||||
src/strategies/reconstruction_pcl_greedy_stage.h \
|
||||
src/strategies/reconstruction_surface_stage.h \
|
||||
src/strategies/transform_tf_stage.h \
|
||||
src/tests/pipeline_smoke_tests.h \
|
||||
src/ui/mainwindow.h \
|
||||
src/ui/glview.h
|
||||
|
||||
RESOURCES += \
|
||||
src/ui/qml/ui_qml.qrc
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
QT += core
|
||||
|
||||
CONFIG += console c++14
|
||||
CONFIG -= app_bundle
|
||||
TEMPLATE = app
|
||||
TARGET = DotsToSirfaceTests
|
||||
|
||||
contains(DEFINES, PCL_ENABLED) {
|
||||
message(PCL support enabled)
|
||||
win32 {
|
||||
isEmpty(PCL_ROOT) {
|
||||
PCL_ROOT = C:/PCL
|
||||
}
|
||||
INCLUDEPATH += $$PCL_ROOT/include $$PCL_ROOT/include/pcl-1.12
|
||||
LIBS += -L$$PCL_ROOT/lib \
|
||||
-lpcl_common \
|
||||
-lpcl_io \
|
||||
-lpcl_filters \
|
||||
-lpcl_features \
|
||||
-lpcl_kdtree \
|
||||
-lpcl_search \
|
||||
-lpcl_surface
|
||||
}
|
||||
|
||||
unix {
|
||||
CONFIG += link_pkgconfig
|
||||
PKGCONFIG += \
|
||||
pcl_common \
|
||||
pcl_io \
|
||||
pcl_filters \
|
||||
pcl_features \
|
||||
pcl_kdtree \
|
||||
pcl_search \
|
||||
pcl_surface
|
||||
}
|
||||
}
|
||||
|
||||
SOURCES += \
|
||||
src/tests/test_runner_main.cpp \
|
||||
src/tests/pipeline_smoke_tests.cpp \
|
||||
src/algorithms/preprocess/preprocess_algorithms.cpp \
|
||||
src/algorithms/reconstruction/surface_reconstruction.cpp \
|
||||
src/adapters/reconstruction/reconstruction_adapter.cpp \
|
||||
src/adapters/pcl/pcl_point_cloud_adapter.cpp \
|
||||
src/adapters/registration/registration_adapter.cpp \
|
||||
src/adapters/ros2/pointcloud2_adapter.cpp \
|
||||
src/adapters/ros2/tf2_adapter.cpp \
|
||||
src/adapters/sources/file_point_cloud_source.cpp \
|
||||
src/adapters/sources/ros2_point_cloud_source.cpp \
|
||||
src/core/pipeline_config.cpp \
|
||||
src/core/pipeline_executor.cpp \
|
||||
src/core/pipeline_plugin_registry.cpp \
|
||||
src/factories/pipeline/desktop_pipeline_factory.cpp \
|
||||
src/strategies/preprocess_basic_stages.cpp \
|
||||
src/strategies/preprocess_pcl_stages.cpp \
|
||||
src/strategies/registration_icp_stage.cpp \
|
||||
src/strategies/transform_tf_stage.cpp
|
||||
|
||||
HEADERS += \
|
||||
src/tests/pipeline_smoke_tests.h \
|
||||
src/algorithms/preprocess/preprocess_algorithms.h \
|
||||
src/algorithms/reconstruction/surface_reconstruction.h \
|
||||
src/adapters/reconstruction/reconstruction_adapter.h \
|
||||
src/adapters/pcl/pcl_point_cloud_adapter.h \
|
||||
src/adapters/registration/registration_adapter.h \
|
||||
src/adapters/ros2/pointcloud2_adapter.h \
|
||||
src/adapters/ros2/tf2_adapter.h \
|
||||
src/adapters/sources/file_point_cloud_source.h \
|
||||
src/adapters/sources/ros2_point_cloud_source.h \
|
||||
src/core/data_source.h \
|
||||
src/core/point_cloud_types.h \
|
||||
src/core/pipeline_config.h \
|
||||
src/core/pipeline_stage.h \
|
||||
src/core/pipeline_executor.h \
|
||||
src/core/pipeline_plugin_registry.h \
|
||||
src/factories/pipeline/desktop_pipeline_factory.h \
|
||||
src/strategies/preprocess_basic_stages.h \
|
||||
src/strategies/preprocess_pcl_stages.h \
|
||||
src/strategies/reconstruction_surface_stage.h \
|
||||
src/strategies/reconstruction_pcl_greedy_stage.h \
|
||||
src/strategies/registration_icp_stage.h \
|
||||
src/strategies/transform_tf_stage.h
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
Демонстрационная программа на Qt 5.11 C++, которая:
|
||||
- принимает массив 3D-точек `QVector<Point3f>`;
|
||||
- строит массив треугольников `QVector<Triangle>`;
|
||||
- выполняет stage-based pipeline (preprocess -> transform -> registration -> reconstruction);
|
||||
- визуализирует точки и полученную треугольную поверхность.
|
||||
|
||||
## Вход/выход API
|
||||
|
||||
`src/geometry/surface_reconstruction.h`:
|
||||
`src/algorithms/reconstruction/surface_reconstruction.h`:
|
||||
|
||||
- `struct Point3f { float x, y, z; };`
|
||||
- `struct Triangle { int i0, i1, i2; };`
|
||||
@@ -28,6 +28,22 @@
|
||||
Текущая реализация строит **выпуклую оболочку** облака точек.
|
||||
Для невыпуклых объектов и детальной реконструкции произвольной поверхности нужны более сложные алгоритмы (например, alpha-shapes, Poisson reconstruction и т.п.).
|
||||
|
||||
## Архитектурный каркас под robotics pipeline
|
||||
|
||||
- `core`: контракты (`PointCloudFrame`, `PipelineContext`, `PipelineStats`, stage-интерфейсы, plugin registry).
|
||||
- `adapters`: мосты к источникам и внешним транспортам (`FilePointCloudSource`, `Ros2PointCloudSource`, `PointCloud2`/`tf2` adapters).
|
||||
- `strategies`: конкретные стадии пайплайна (preprocess, transform, registration, reconstruction).
|
||||
- `factories`: сборка `PipelineExecutor` из plugin-id и профиля.
|
||||
|
||||
### Профили выполнения
|
||||
|
||||
- `desktop_debug`: дефолтный профиль для GUI/отладки.
|
||||
- `rpi4_runtime`: профиль для Raspberry Pi 4 (жестче downsampling, лимит точек, reconstruction реже, async-friendly настройки).
|
||||
|
||||
Выбор профиля:
|
||||
- `factories::pipeline::createPipelineExecutorForProfile("desktop_debug")`
|
||||
- `factories::pipeline::createPipelineExecutorForProfile("rpi4_runtime")`
|
||||
|
||||
## Сборка
|
||||
|
||||
Пример для Qt 5.11:
|
||||
@@ -39,6 +55,19 @@ make
|
||||
|
||||
Для Windows/MSVC используйте соответствующий `nmake`/`jom`.
|
||||
|
||||
## Smoke tests (отдельный runner)
|
||||
|
||||
Для быстрой проверки пайплайна без GUI добавлен отдельный консольный таргет:
|
||||
`DotsToSirfaceTests.pro`.
|
||||
|
||||
```bash
|
||||
qmake DotsToSirfaceTests.pro
|
||||
make
|
||||
./release/DotsToSirfaceTests.exe
|
||||
```
|
||||
|
||||
При успешном запуске runner печатает `Smoke tests passed.` и завершаетcя с кодом `0`.
|
||||
|
||||
## Демо
|
||||
|
||||
При запуске приложение:
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
#include "pcl_point_cloud_adapter.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
#ifdef PCL_ENABLED
|
||||
#include <pcl/common/io.h>
|
||||
#include <pcl/features/normal_3d.h>
|
||||
#include <pcl/filters/radius_outlier_removal.h>
|
||||
#include <pcl/filters/statistical_outlier_removal.h>
|
||||
#include <pcl/filters/voxel_grid.h>
|
||||
#include <pcl/kdtree/kdtree_flann.h>
|
||||
#include <pcl/point_cloud.h>
|
||||
#include <pcl/point_types.h>
|
||||
#include <pcl/search/kdtree.h>
|
||||
#include <pcl/surface/gp3.h>
|
||||
#endif
|
||||
|
||||
namespace adapters
|
||||
{
|
||||
namespace pcl
|
||||
{
|
||||
namespace
|
||||
{
|
||||
#ifdef PCL_ENABLED
|
||||
::pcl::PointCloud<::pcl::PointXYZ>::Ptr toPclCloud(const QVector<core::Point3f> &points)
|
||||
{
|
||||
::pcl::PointCloud<::pcl::PointXYZ>::Ptr cloud(new ::pcl::PointCloud<::pcl::PointXYZ>());
|
||||
cloud->reserve(static_cast<std::size_t>(points.size()));
|
||||
for (const core::Point3f &p : points) {
|
||||
cloud->push_back(::pcl::PointXYZ(p.x, p.y, p.z));
|
||||
}
|
||||
return cloud;
|
||||
}
|
||||
|
||||
QVector<core::Point3f> fromPclCloud(const ::pcl::PointCloud<::pcl::PointXYZ>::Ptr &cloud)
|
||||
{
|
||||
QVector<core::Point3f> points;
|
||||
points.reserve(static_cast<int>(cloud->size()));
|
||||
for (std::size_t i = 0; i < cloud->size(); ++i) {
|
||||
const ::pcl::PointXYZ &p = cloud->at(i);
|
||||
points.push_back({p.x, p.y, p.z});
|
||||
}
|
||||
return points;
|
||||
}
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
QVector<core::Point3f> passThroughPoints(const QVector<core::Point3f> &points)
|
||||
{
|
||||
return points;
|
||||
}
|
||||
|
||||
QVector<core::Point3f> applyVoxelGrid(
|
||||
const QVector<core::Point3f> &points,
|
||||
const float leafSize,
|
||||
int &removedCount)
|
||||
{
|
||||
#ifdef PCL_ENABLED
|
||||
::pcl::PointCloud<::pcl::PointXYZ>::Ptr in = toPclCloud(points);
|
||||
::pcl::PointCloud<::pcl::PointXYZ>::Ptr out(new ::pcl::PointCloud<::pcl::PointXYZ>());
|
||||
::pcl::VoxelGrid<::pcl::PointXYZ> filter;
|
||||
filter.setInputCloud(in);
|
||||
filter.setLeafSize(leafSize, leafSize, leafSize);
|
||||
filter.filter(*out);
|
||||
removedCount = points.size() - static_cast<int>(out->size());
|
||||
return fromPclCloud(out);
|
||||
#else
|
||||
Q_UNUSED(leafSize);
|
||||
removedCount = 0;
|
||||
return points;
|
||||
#endif
|
||||
}
|
||||
|
||||
QVector<core::Point3f> applyStatisticalOutlierRemoval(
|
||||
const QVector<core::Point3f> &points,
|
||||
const int meanK,
|
||||
const double stdDevMulThresh,
|
||||
int &removedCount)
|
||||
{
|
||||
#ifdef PCL_ENABLED
|
||||
::pcl::PointCloud<::pcl::PointXYZ>::Ptr in = toPclCloud(points);
|
||||
::pcl::PointCloud<::pcl::PointXYZ>::Ptr out(new ::pcl::PointCloud<::pcl::PointXYZ>());
|
||||
::pcl::StatisticalOutlierRemoval<::pcl::PointXYZ> filter;
|
||||
filter.setInputCloud(in);
|
||||
filter.setMeanK(meanK);
|
||||
filter.setStddevMulThresh(stdDevMulThresh);
|
||||
filter.filter(*out);
|
||||
removedCount = points.size() - static_cast<int>(out->size());
|
||||
return fromPclCloud(out);
|
||||
#else
|
||||
Q_UNUSED(meanK);
|
||||
Q_UNUSED(stdDevMulThresh);
|
||||
removedCount = 0;
|
||||
return points;
|
||||
#endif
|
||||
}
|
||||
|
||||
QVector<core::Point3f> applyRadiusOutlierRemoval(
|
||||
const QVector<core::Point3f> &points,
|
||||
const float radius,
|
||||
const int minNeighbors,
|
||||
int &removedCount)
|
||||
{
|
||||
#ifdef PCL_ENABLED
|
||||
::pcl::PointCloud<::pcl::PointXYZ>::Ptr in = toPclCloud(points);
|
||||
::pcl::PointCloud<::pcl::PointXYZ>::Ptr out(new ::pcl::PointCloud<::pcl::PointXYZ>());
|
||||
::pcl::RadiusOutlierRemoval<::pcl::PointXYZ> filter;
|
||||
filter.setInputCloud(in);
|
||||
filter.setRadiusSearch(radius);
|
||||
filter.setMinNeighborsInRadius(minNeighbors);
|
||||
filter.filter(*out);
|
||||
removedCount = points.size() - static_cast<int>(out->size());
|
||||
return fromPclCloud(out);
|
||||
#else
|
||||
Q_UNUSED(radius);
|
||||
Q_UNUSED(minNeighbors);
|
||||
removedCount = 0;
|
||||
return points;
|
||||
#endif
|
||||
}
|
||||
|
||||
QVector<core::Triangle> buildGreedyTriangles(
|
||||
const QVector<core::Point3f> &points,
|
||||
const float searchRadius,
|
||||
const float mu,
|
||||
const int maxNearestNeighbors,
|
||||
const float maxSurfaceAngleRadians)
|
||||
{
|
||||
#ifdef PCL_ENABLED
|
||||
if (points.size() < 4) {
|
||||
return QVector<core::Triangle>();
|
||||
}
|
||||
|
||||
::pcl::PointCloud<::pcl::PointXYZ>::Ptr cloud = toPclCloud(points);
|
||||
::pcl::search::KdTree<::pcl::PointXYZ>::Ptr tree(new ::pcl::search::KdTree<::pcl::PointXYZ>());
|
||||
|
||||
::pcl::PointCloud<::pcl::Normal>::Ptr normals(new ::pcl::PointCloud<::pcl::Normal>());
|
||||
::pcl::NormalEstimation<::pcl::PointXYZ, ::pcl::Normal> n;
|
||||
n.setInputCloud(cloud);
|
||||
n.setSearchMethod(tree);
|
||||
n.setKSearch(20);
|
||||
n.compute(*normals);
|
||||
|
||||
::pcl::PointCloud<::pcl::PointNormal>::Ptr cloudWithNormals(new ::pcl::PointCloud<::pcl::PointNormal>());
|
||||
::pcl::concatenateFields(*cloud, *normals, *cloudWithNormals);
|
||||
|
||||
::pcl::search::KdTree<::pcl::PointNormal>::Ptr tree2(new ::pcl::search::KdTree<::pcl::PointNormal>());
|
||||
::pcl::GreedyProjectionTriangulation<::pcl::PointNormal> gp3;
|
||||
gp3.setSearchRadius(searchRadius);
|
||||
gp3.setMu(mu);
|
||||
gp3.setMaximumNearestNeighbors(maxNearestNeighbors);
|
||||
gp3.setMaximumSurfaceAngle(maxSurfaceAngleRadians);
|
||||
gp3.setMinimumAngle(0.1);
|
||||
gp3.setMaximumAngle(2.5);
|
||||
gp3.setNormalConsistency(false);
|
||||
gp3.setInputCloud(cloudWithNormals);
|
||||
gp3.setSearchMethod(tree2);
|
||||
|
||||
::pcl::PolygonMesh mesh;
|
||||
gp3.reconstruct(mesh);
|
||||
|
||||
QVector<core::Triangle> triangles;
|
||||
triangles.reserve(static_cast<int>(mesh.polygons.size()));
|
||||
for (std::size_t i = 0; i < mesh.polygons.size(); ++i) {
|
||||
const ::pcl::Vertices &v = mesh.polygons[i];
|
||||
if (v.vertices.size() < 3) {
|
||||
continue;
|
||||
}
|
||||
core::Triangle triangle = {
|
||||
static_cast<int>(v.vertices[0]),
|
||||
static_cast<int>(v.vertices[1]),
|
||||
static_cast<int>(v.vertices[2])};
|
||||
triangles.push_back(triangle);
|
||||
}
|
||||
return triangles;
|
||||
#else
|
||||
Q_UNUSED(searchRadius);
|
||||
Q_UNUSED(mu);
|
||||
Q_UNUSED(maxNearestNeighbors);
|
||||
Q_UNUSED(maxSurfaceAngleRadians);
|
||||
Q_UNUSED(points);
|
||||
return QVector<core::Triangle>();
|
||||
#endif
|
||||
}
|
||||
} // namespace pcl
|
||||
} // namespace adapters
|
||||
@@ -0,0 +1,34 @@
|
||||
#ifndef PCL_POINT_CLOUD_ADAPTER_H
|
||||
#define PCL_POINT_CLOUD_ADAPTER_H
|
||||
|
||||
#include "../../core/point_cloud_types.h"
|
||||
|
||||
namespace adapters
|
||||
{
|
||||
namespace pcl
|
||||
{
|
||||
QVector<core::Point3f> passThroughPoints(const QVector<core::Point3f> &points);
|
||||
QVector<core::Point3f> applyVoxelGrid(
|
||||
const QVector<core::Point3f> &points,
|
||||
float leafSize,
|
||||
int &removedCount);
|
||||
QVector<core::Point3f> applyStatisticalOutlierRemoval(
|
||||
const QVector<core::Point3f> &points,
|
||||
int meanK,
|
||||
double stdDevMulThresh,
|
||||
int &removedCount);
|
||||
QVector<core::Point3f> applyRadiusOutlierRemoval(
|
||||
const QVector<core::Point3f> &points,
|
||||
float radius,
|
||||
int minNeighbors,
|
||||
int &removedCount);
|
||||
QVector<core::Triangle> buildGreedyTriangles(
|
||||
const QVector<core::Point3f> &points,
|
||||
float searchRadius,
|
||||
float mu,
|
||||
int maxNearestNeighbors,
|
||||
float maxSurfaceAngleRadians);
|
||||
} // namespace pcl
|
||||
} // namespace adapters
|
||||
|
||||
#endif // PCL_POINT_CLOUD_ADAPTER_H
|
||||
@@ -0,0 +1,32 @@
|
||||
#include "reconstruction_adapter.h"
|
||||
|
||||
#include "../../algorithms/reconstruction/surface_reconstruction.h"
|
||||
|
||||
namespace adapters
|
||||
{
|
||||
namespace reconstruction
|
||||
{
|
||||
QVector<core::Triangle> buildSurfaceTriangles(
|
||||
const QVector<core::Point3f> &points,
|
||||
const core::ReconstructionConfig &config)
|
||||
{
|
||||
algorithms::reconstruction::setNeighborRadiusScale(config.neighborRadiusScale);
|
||||
|
||||
QVector<algorithms::reconstruction::Point3f> algorithmPoints;
|
||||
algorithmPoints.reserve(points.size());
|
||||
for (const core::Point3f &p : points) {
|
||||
algorithmPoints.push_back({p.x, p.y, p.z});
|
||||
}
|
||||
|
||||
const QVector<algorithms::reconstruction::Triangle> algorithmTriangles =
|
||||
algorithms::reconstruction::buildSurfaceTriangles(algorithmPoints);
|
||||
|
||||
QVector<core::Triangle> result;
|
||||
result.reserve(algorithmTriangles.size());
|
||||
for (const algorithms::reconstruction::Triangle &t : algorithmTriangles) {
|
||||
result.push_back({t.i0, t.i1, t.i2});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
} // namespace reconstruction
|
||||
} // namespace adapters
|
||||
@@ -0,0 +1,17 @@
|
||||
#ifndef RECONSTRUCTION_ADAPTER_H
|
||||
#define RECONSTRUCTION_ADAPTER_H
|
||||
|
||||
#include "../../core/pipeline_config.h"
|
||||
#include "../../core/point_cloud_types.h"
|
||||
|
||||
namespace adapters
|
||||
{
|
||||
namespace reconstruction
|
||||
{
|
||||
QVector<core::Triangle> buildSurfaceTriangles(
|
||||
const QVector<core::Point3f> &points,
|
||||
const core::ReconstructionConfig &config);
|
||||
} // namespace reconstruction
|
||||
} // namespace adapters
|
||||
|
||||
#endif // RECONSTRUCTION_ADAPTER_H
|
||||
@@ -0,0 +1,51 @@
|
||||
#include "registration_adapter.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
namespace
|
||||
{
|
||||
core::Point3f centroid(const QVector<core::Point3f> &points)
|
||||
{
|
||||
core::Point3f c{0.0f, 0.0f, 0.0f};
|
||||
if (points.isEmpty()) {
|
||||
return c;
|
||||
}
|
||||
for (const core::Point3f &p : points) {
|
||||
c.x += p.x;
|
||||
c.y += p.y;
|
||||
c.z += p.z;
|
||||
}
|
||||
const float inv = 1.0f / static_cast<float>(points.size());
|
||||
c.x *= inv;
|
||||
c.y *= inv;
|
||||
c.z *= inv;
|
||||
return c;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace adapters
|
||||
{
|
||||
namespace registration
|
||||
{
|
||||
RegistrationResult alignToReference(
|
||||
const core::PointCloudFrame &input,
|
||||
const core::RegistrationConfig &config)
|
||||
{
|
||||
Q_UNUSED(config);
|
||||
|
||||
RegistrationResult result;
|
||||
result.alignedFrame = input;
|
||||
|
||||
// Fallback implementation for environments without PCL.
|
||||
const core::Point3f c = centroid(result.alignedFrame.points);
|
||||
for (int i = 0; i < result.alignedFrame.points.size(); ++i) {
|
||||
result.alignedFrame.points[i].x -= c.x;
|
||||
result.alignedFrame.points[i].y -= c.y;
|
||||
result.alignedFrame.points[i].z -= c.z;
|
||||
}
|
||||
result.fitness = 1.0;
|
||||
result.rmse = 0.0;
|
||||
return result;
|
||||
}
|
||||
} // namespace registration
|
||||
} // namespace adapters
|
||||
@@ -0,0 +1,24 @@
|
||||
#ifndef REGISTRATION_ADAPTER_H
|
||||
#define REGISTRATION_ADAPTER_H
|
||||
|
||||
#include "../../core/pipeline_config.h"
|
||||
#include "../../core/point_cloud_types.h"
|
||||
|
||||
namespace adapters
|
||||
{
|
||||
namespace registration
|
||||
{
|
||||
struct RegistrationResult
|
||||
{
|
||||
core::PointCloudFrame alignedFrame;
|
||||
double fitness = 0.0;
|
||||
double rmse = 0.0;
|
||||
};
|
||||
|
||||
RegistrationResult alignToReference(
|
||||
const core::PointCloudFrame &input,
|
||||
const core::RegistrationConfig &config);
|
||||
} // namespace registration
|
||||
} // namespace adapters
|
||||
|
||||
#endif // REGISTRATION_ADAPTER_H
|
||||
@@ -0,0 +1,17 @@
|
||||
#include "pointcloud2_adapter.h"
|
||||
|
||||
namespace adapters
|
||||
{
|
||||
namespace ros2
|
||||
{
|
||||
core::PointCloudFrame fromPointCloud2(const PointCloud2Message &message)
|
||||
{
|
||||
core::PointCloudFrame frame;
|
||||
frame.points = message.xyz;
|
||||
frame.frameId = message.frameId;
|
||||
frame.timestampUsec = message.timestampUsec;
|
||||
frame.sourceLabel = "ros2:PointCloud2";
|
||||
return frame;
|
||||
}
|
||||
} // namespace ros2
|
||||
} // namespace adapters
|
||||
@@ -0,0 +1,21 @@
|
||||
#ifndef POINTCLOUD2_ADAPTER_H
|
||||
#define POINTCLOUD2_ADAPTER_H
|
||||
|
||||
#include "../../core/point_cloud_types.h"
|
||||
|
||||
namespace adapters
|
||||
{
|
||||
namespace ros2
|
||||
{
|
||||
struct PointCloud2Message
|
||||
{
|
||||
QVector<core::Point3f> xyz;
|
||||
QString frameId;
|
||||
qint64 timestampUsec = 0;
|
||||
};
|
||||
|
||||
core::PointCloudFrame fromPointCloud2(const PointCloud2Message &message);
|
||||
} // namespace ros2
|
||||
} // namespace adapters
|
||||
|
||||
#endif // POINTCLOUD2_ADAPTER_H
|
||||
@@ -0,0 +1,19 @@
|
||||
#include "tf2_adapter.h"
|
||||
|
||||
namespace adapters
|
||||
{
|
||||
namespace ros2
|
||||
{
|
||||
core::PointCloudFrame transformFrame(const core::PointCloudFrame &input, const TfTransform &transform)
|
||||
{
|
||||
core::PointCloudFrame output = input;
|
||||
output.frameId = transform.toFrame;
|
||||
for (int i = 0; i < output.points.size(); ++i) {
|
||||
output.points[i].x += transform.tx;
|
||||
output.points[i].y += transform.ty;
|
||||
output.points[i].z += transform.tz;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
} // namespace ros2
|
||||
} // namespace adapters
|
||||
@@ -0,0 +1,23 @@
|
||||
#ifndef TF2_ADAPTER_H
|
||||
#define TF2_ADAPTER_H
|
||||
|
||||
#include "../../core/point_cloud_types.h"
|
||||
|
||||
namespace adapters
|
||||
{
|
||||
namespace ros2
|
||||
{
|
||||
struct TfTransform
|
||||
{
|
||||
QString fromFrame;
|
||||
QString toFrame;
|
||||
float tx = 0.0f;
|
||||
float ty = 0.0f;
|
||||
float tz = 0.0f;
|
||||
};
|
||||
|
||||
core::PointCloudFrame transformFrame(const core::PointCloudFrame &input, const TfTransform &transform);
|
||||
} // namespace ros2
|
||||
} // namespace adapters
|
||||
|
||||
#endif // TF2_ADAPTER_H
|
||||
@@ -0,0 +1,82 @@
|
||||
#include "file_point_cloud_source.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QRegularExpression>
|
||||
#include <QTextStream>
|
||||
|
||||
namespace adapters
|
||||
{
|
||||
namespace sources
|
||||
{
|
||||
FilePointCloudSource::FilePointCloudSource(const QString &filePath)
|
||||
: m_filePath(filePath)
|
||||
{
|
||||
}
|
||||
|
||||
bool FilePointCloudSource::nextFrame(core::PointCloudFrame &frame, QString &errorText)
|
||||
{
|
||||
if (m_consumed) {
|
||||
errorText = "End of file source.";
|
||||
return false;
|
||||
}
|
||||
|
||||
QFile file(m_filePath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
errorText = QString("Cannot open file: %1").arg(m_filePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
QVector<core::Point3f> points;
|
||||
const QString ext = QFileInfo(m_filePath).suffix().toLower();
|
||||
if (ext == "bin") {
|
||||
const QByteArray raw = file.readAll();
|
||||
if (raw.size() % static_cast<int>(sizeof(float) * 3) != 0) {
|
||||
errorText = "Invalid .bin size.";
|
||||
return false;
|
||||
}
|
||||
const int n = raw.size() / static_cast<int>(sizeof(float) * 3);
|
||||
points.reserve(n);
|
||||
const float *values = reinterpret_cast<const float *>(raw.constData());
|
||||
for (int i = 0; i < n; ++i) {
|
||||
const int k = i * 3;
|
||||
points.push_back({values[k], values[k + 1], values[k + 2]});
|
||||
}
|
||||
} else {
|
||||
QTextStream stream(&file);
|
||||
while (!stream.atEnd()) {
|
||||
QString line = stream.readLine().trimmed();
|
||||
if (line.isEmpty() || line.startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
line.replace(';', ' ');
|
||||
line.replace(',', ' ');
|
||||
const QStringList parts = line.split(QRegularExpression("\\s+"), QString::SkipEmptyParts);
|
||||
if (parts.size() < 3) {
|
||||
continue;
|
||||
}
|
||||
bool okX = false;
|
||||
bool okY = false;
|
||||
bool okZ = false;
|
||||
const float x = parts[0].toFloat(&okX);
|
||||
const float y = parts[1].toFloat(&okY);
|
||||
const float z = parts[2].toFloat(&okZ);
|
||||
if (okX && okY && okZ) {
|
||||
points.push_back({x, y, z});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (points.size() < 4) {
|
||||
errorText = "Need at least 4 points.";
|
||||
return false;
|
||||
}
|
||||
|
||||
frame.points = points;
|
||||
frame.sourceLabel = QFileInfo(m_filePath).fileName();
|
||||
frame.frameId = "sensor";
|
||||
m_consumed = true;
|
||||
return true;
|
||||
}
|
||||
} // namespace sources
|
||||
} // namespace adapters
|
||||
@@ -0,0 +1,23 @@
|
||||
#ifndef FILE_POINT_CLOUD_SOURCE_H
|
||||
#define FILE_POINT_CLOUD_SOURCE_H
|
||||
|
||||
#include "../../core/data_source.h"
|
||||
|
||||
namespace adapters
|
||||
{
|
||||
namespace sources
|
||||
{
|
||||
class FilePointCloudSource : public core::IDataSource
|
||||
{
|
||||
public:
|
||||
explicit FilePointCloudSource(const QString &filePath);
|
||||
bool nextFrame(core::PointCloudFrame &frame, QString &errorText) override;
|
||||
|
||||
private:
|
||||
QString m_filePath;
|
||||
bool m_consumed = false;
|
||||
};
|
||||
} // namespace sources
|
||||
} // namespace adapters
|
||||
|
||||
#endif // FILE_POINT_CLOUD_SOURCE_H
|
||||
@@ -0,0 +1,27 @@
|
||||
#include "ros2_point_cloud_source.h"
|
||||
|
||||
namespace adapters
|
||||
{
|
||||
namespace sources
|
||||
{
|
||||
Ros2PointCloudSource::Ros2PointCloudSource()
|
||||
{
|
||||
}
|
||||
|
||||
void Ros2PointCloudSource::pushMockMessage(const ros2::PointCloud2Message &message)
|
||||
{
|
||||
m_queue.push_back(message);
|
||||
}
|
||||
|
||||
bool Ros2PointCloudSource::nextFrame(core::PointCloudFrame &frame, QString &errorText)
|
||||
{
|
||||
if (m_queue.isEmpty()) {
|
||||
errorText = "ROS2 source has no frames.";
|
||||
return false;
|
||||
}
|
||||
frame = ros2::fromPointCloud2(m_queue.front());
|
||||
m_queue.pop_front();
|
||||
return true;
|
||||
}
|
||||
} // namespace sources
|
||||
} // namespace adapters
|
||||
@@ -0,0 +1,24 @@
|
||||
#ifndef ROS2_POINT_CLOUD_SOURCE_H
|
||||
#define ROS2_POINT_CLOUD_SOURCE_H
|
||||
|
||||
#include "../../core/data_source.h"
|
||||
#include "../ros2/pointcloud2_adapter.h"
|
||||
|
||||
namespace adapters
|
||||
{
|
||||
namespace sources
|
||||
{
|
||||
class Ros2PointCloudSource : public core::IDataSource
|
||||
{
|
||||
public:
|
||||
Ros2PointCloudSource();
|
||||
void pushMockMessage(const ros2::PointCloud2Message &message);
|
||||
bool nextFrame(core::PointCloudFrame &frame, QString &errorText) override;
|
||||
|
||||
private:
|
||||
QVector<ros2::PointCloud2Message> m_queue;
|
||||
};
|
||||
} // namespace sources
|
||||
} // namespace adapters
|
||||
|
||||
#endif // ROS2_POINT_CLOUD_SOURCE_H
|
||||
@@ -0,0 +1,214 @@
|
||||
#include "preprocess_algorithms.h"
|
||||
|
||||
#include <QHash>
|
||||
#include <QString>
|
||||
#include <QtGlobal>
|
||||
#include <QtMath>
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
|
||||
namespace
|
||||
{
|
||||
struct Dsu
|
||||
{
|
||||
QVector<int> parent;
|
||||
QVector<int> size;
|
||||
|
||||
explicit Dsu(const int n)
|
||||
: parent(n)
|
||||
, size(n, 1)
|
||||
{
|
||||
for (int i = 0; i < n; ++i) {
|
||||
parent[i] = i;
|
||||
}
|
||||
}
|
||||
|
||||
int find(int x)
|
||||
{
|
||||
while (parent[x] != x) {
|
||||
parent[x] = parent[parent[x]];
|
||||
x = parent[x];
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
void unite(const int a, const int b)
|
||||
{
|
||||
int ra = find(a);
|
||||
int rb = find(b);
|
||||
if (ra == rb) {
|
||||
return;
|
||||
}
|
||||
if (size[ra] < size[rb]) {
|
||||
qSwap(ra, rb);
|
||||
}
|
||||
parent[rb] = ra;
|
||||
size[ra] += size[rb];
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
namespace algorithms
|
||||
{
|
||||
namespace preprocess
|
||||
{
|
||||
QVector<core::Point3f> keepLargestCluster(
|
||||
const QVector<core::Point3f> &points,
|
||||
int &removedCount,
|
||||
int &clusterCount,
|
||||
const double clusterJoinDistanceScale)
|
||||
{
|
||||
removedCount = 0;
|
||||
clusterCount = 1;
|
||||
if (points.size() < 8) {
|
||||
return points;
|
||||
}
|
||||
|
||||
QVector<double> nearestDist;
|
||||
nearestDist.reserve(points.size());
|
||||
for (int i = 0; i < points.size(); ++i) {
|
||||
double best = std::numeric_limits<double>::max();
|
||||
for (int j = 0; j < points.size(); ++j) {
|
||||
if (i == j) {
|
||||
continue;
|
||||
}
|
||||
const double dx = static_cast<double>(points[i].x) - static_cast<double>(points[j].x);
|
||||
const double dy = static_cast<double>(points[i].y) - static_cast<double>(points[j].y);
|
||||
const double dz = static_cast<double>(points[i].z) - static_cast<double>(points[j].z);
|
||||
const double d = qSqrt(dx * dx + dy * dy + dz * dz);
|
||||
if (d < best) {
|
||||
best = d;
|
||||
}
|
||||
}
|
||||
if (best < std::numeric_limits<double>::max()) {
|
||||
nearestDist.push_back(best);
|
||||
}
|
||||
}
|
||||
if (nearestDist.isEmpty()) {
|
||||
return points;
|
||||
}
|
||||
|
||||
std::sort(nearestDist.begin(), nearestDist.end());
|
||||
const double medianNn = nearestDist[nearestDist.size() / 2];
|
||||
const double joinDistance = qMax(1e-9, medianNn * clusterJoinDistanceScale);
|
||||
const double joinDistance2 = joinDistance * joinDistance;
|
||||
|
||||
Dsu dsu(points.size());
|
||||
for (int i = 0; i < points.size(); ++i) {
|
||||
for (int j = i + 1; j < points.size(); ++j) {
|
||||
const double dx = static_cast<double>(points[i].x) - static_cast<double>(points[j].x);
|
||||
const double dy = static_cast<double>(points[i].y) - static_cast<double>(points[j].y);
|
||||
const double dz = static_cast<double>(points[i].z) - static_cast<double>(points[j].z);
|
||||
const double d2 = dx * dx + dy * dy + dz * dz;
|
||||
if (d2 <= joinDistance2) {
|
||||
dsu.unite(i, j);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QHash<int, int> compSizes;
|
||||
for (int i = 0; i < points.size(); ++i) {
|
||||
const int root = dsu.find(i);
|
||||
compSizes[root] = compSizes.value(root, 0) + 1;
|
||||
}
|
||||
clusterCount = compSizes.size();
|
||||
|
||||
int largestRoot = -1;
|
||||
int largestSize = 0;
|
||||
for (auto it = compSizes.constBegin(); it != compSizes.constEnd(); ++it) {
|
||||
if (it.value() > largestSize) {
|
||||
largestSize = it.value();
|
||||
largestRoot = it.key();
|
||||
}
|
||||
}
|
||||
|
||||
QVector<core::Point3f> filtered;
|
||||
filtered.reserve(largestSize);
|
||||
for (int i = 0; i < points.size(); ++i) {
|
||||
if (dsu.find(i) == largestRoot) {
|
||||
filtered.push_back(points[i]);
|
||||
}
|
||||
}
|
||||
|
||||
removedCount = points.size() - filtered.size();
|
||||
return filtered;
|
||||
}
|
||||
|
||||
QVector<core::Point3f> downsampleDenseAreas(
|
||||
const QVector<core::Point3f> &points,
|
||||
int &removedCount,
|
||||
const double downsampleCellScale)
|
||||
{
|
||||
removedCount = 0;
|
||||
if (points.size() < 16) {
|
||||
return points;
|
||||
}
|
||||
|
||||
QVector<double> nearestXY;
|
||||
nearestXY.reserve(points.size());
|
||||
for (int i = 0; i < points.size(); ++i) {
|
||||
double best = std::numeric_limits<double>::max();
|
||||
for (int j = 0; j < points.size(); ++j) {
|
||||
if (i == j) {
|
||||
continue;
|
||||
}
|
||||
const double dx = static_cast<double>(points[i].x) - static_cast<double>(points[j].x);
|
||||
const double dy = static_cast<double>(points[i].y) - static_cast<double>(points[j].y);
|
||||
const double d = qSqrt(dx * dx + dy * dy);
|
||||
if (d < best) {
|
||||
best = d;
|
||||
}
|
||||
}
|
||||
if (best < std::numeric_limits<double>::max()) {
|
||||
nearestXY.push_back(best);
|
||||
}
|
||||
}
|
||||
if (nearestXY.isEmpty()) {
|
||||
return points;
|
||||
}
|
||||
|
||||
std::sort(nearestXY.begin(), nearestXY.end());
|
||||
const double medianXY = nearestXY[nearestXY.size() / 2];
|
||||
const double cellSize = qMax(1e-9, medianXY * downsampleCellScale);
|
||||
|
||||
struct Bucket
|
||||
{
|
||||
double sx;
|
||||
double sy;
|
||||
double sz;
|
||||
int n;
|
||||
};
|
||||
QHash<QString, Bucket> buckets;
|
||||
buckets.reserve(points.size());
|
||||
|
||||
for (const core::Point3f &p : points) {
|
||||
const qint64 ix = qFloor(static_cast<double>(p.x) / cellSize);
|
||||
const qint64 iy = qFloor(static_cast<double>(p.y) / cellSize);
|
||||
const QString key = QString::number(ix) + "_" + QString::number(iy);
|
||||
if (!buckets.contains(key)) {
|
||||
buckets.insert(key, {p.x, p.y, p.z, 1});
|
||||
} else {
|
||||
Bucket &b = buckets[key];
|
||||
b.sx += p.x;
|
||||
b.sy += p.y;
|
||||
b.sz += p.z;
|
||||
b.n += 1;
|
||||
}
|
||||
}
|
||||
|
||||
QVector<core::Point3f> reduced;
|
||||
reduced.reserve(buckets.size());
|
||||
for (auto it = buckets.constBegin(); it != buckets.constEnd(); ++it) {
|
||||
const Bucket &bucket = it.value();
|
||||
reduced.push_back({
|
||||
static_cast<float>(bucket.sx / bucket.n),
|
||||
static_cast<float>(bucket.sy / bucket.n),
|
||||
static_cast<float>(bucket.sz / bucket.n)
|
||||
});
|
||||
}
|
||||
|
||||
removedCount = points.size() - reduced.size();
|
||||
return reduced;
|
||||
}
|
||||
} // namespace preprocess
|
||||
} // namespace algorithms
|
||||
@@ -0,0 +1,23 @@
|
||||
#ifndef PREPROCESS_ALGORITHMS_H
|
||||
#define PREPROCESS_ALGORITHMS_H
|
||||
|
||||
#include "../../core/point_cloud_types.h"
|
||||
|
||||
namespace algorithms
|
||||
{
|
||||
namespace preprocess
|
||||
{
|
||||
QVector<core::Point3f> keepLargestCluster(
|
||||
const QVector<core::Point3f> &points,
|
||||
int &removedCount,
|
||||
int &clusterCount,
|
||||
double clusterJoinDistanceScale);
|
||||
|
||||
QVector<core::Point3f> downsampleDenseAreas(
|
||||
const QVector<core::Point3f> &points,
|
||||
int &removedCount,
|
||||
double downsampleCellScale);
|
||||
} // namespace preprocess
|
||||
} // namespace algorithms
|
||||
|
||||
#endif // PREPROCESS_ALGORITHMS_H
|
||||
+6
@@ -7,6 +7,10 @@
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
|
||||
namespace algorithms
|
||||
{
|
||||
namespace reconstruction
|
||||
{
|
||||
namespace
|
||||
{
|
||||
const double kEps = 1e-6;
|
||||
@@ -184,3 +188,5 @@ QVector<Triangle> buildSurfaceTriangles(const QVector<Point3f> &points)
|
||||
|
||||
return triangles;
|
||||
}
|
||||
} // namespace reconstruction
|
||||
} // namespace algorithms
|
||||
+6
@@ -3,6 +3,10 @@
|
||||
|
||||
#include <QVector>
|
||||
|
||||
namespace algorithms
|
||||
{
|
||||
namespace reconstruction
|
||||
{
|
||||
struct Point3f
|
||||
{
|
||||
float x;
|
||||
@@ -20,5 +24,7 @@ struct Triangle
|
||||
void setNeighborRadiusScale(float scale);
|
||||
float neighborRadiusScale();
|
||||
QVector<Triangle> buildSurfaceTriangles(const QVector<Point3f> &points);
|
||||
} // namespace reconstruction
|
||||
} // namespace algorithms
|
||||
|
||||
#endif // SURFACE_RECONSTRUCTION_H
|
||||
@@ -0,0 +1,16 @@
|
||||
#ifndef DATA_SOURCE_H
|
||||
#define DATA_SOURCE_H
|
||||
|
||||
#include "point_cloud_types.h"
|
||||
|
||||
namespace core
|
||||
{
|
||||
class IDataSource
|
||||
{
|
||||
public:
|
||||
virtual ~IDataSource() {}
|
||||
virtual bool nextFrame(PointCloudFrame &frame, QString &errorText) = 0;
|
||||
};
|
||||
} // namespace core
|
||||
|
||||
#endif // DATA_SOURCE_H
|
||||
@@ -0,0 +1,38 @@
|
||||
#include "pipeline_config.h"
|
||||
|
||||
namespace core
|
||||
{
|
||||
PipelineConfig makeDesktopDebugConfig()
|
||||
{
|
||||
PipelineConfig config;
|
||||
config.preprocessPlugins = QStringList() << "keep_largest_cluster" << "downsample_dense";
|
||||
config.reconstructionPlugin = "surface_fallback";
|
||||
config.runtime.profile = "desktop_debug";
|
||||
config.runtime.asyncReconstruction = false;
|
||||
config.runtime.targetFps = 20;
|
||||
return config;
|
||||
}
|
||||
|
||||
static PipelineConfig makePclFastConfig()
|
||||
{
|
||||
PipelineConfig config = makeDesktopDebugConfig();
|
||||
config.preprocessPlugins = QStringList() << "pcl_voxel_grid" << "pcl_statistical_outlier";
|
||||
config.reconstructionPlugin = "pcl_greedy_triangulation";
|
||||
config.preprocess.pclVoxelLeafSize = 0.05f;
|
||||
config.preprocess.pclSorMeanK = 20;
|
||||
config.reconstruction.pclGreedySearchRadius = 0.10f;
|
||||
return config;
|
||||
}
|
||||
|
||||
PipelineConfig makeRpi4RuntimeConfig()
|
||||
{
|
||||
PipelineConfig config = makePclFastConfig();
|
||||
config.runtime.profile = "rpi4_runtime";
|
||||
config.runtime.asyncReconstruction = true;
|
||||
config.runtime.targetFps = 10;
|
||||
config.preprocess.maxInputPoints = 60000;
|
||||
config.preprocess.downsampleCellScale = 1.2;
|
||||
config.reconstruction.runEveryNthFrame = 3;
|
||||
return config;
|
||||
}
|
||||
} // namespace core
|
||||
@@ -0,0 +1,69 @@
|
||||
#ifndef PIPELINE_CONFIG_H
|
||||
#define PIPELINE_CONFIG_H
|
||||
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
namespace core
|
||||
{
|
||||
struct PreprocessConfig
|
||||
{
|
||||
double clusterJoinDistanceScale = 5.0;
|
||||
double downsampleCellScale = 0.8;
|
||||
int maxInputPoints = 150000;
|
||||
float pclVoxelLeafSize = 0.04f;
|
||||
int pclSorMeanK = 24;
|
||||
double pclSorStdDevMul = 1.0;
|
||||
float pclRorRadius = 0.08f;
|
||||
int pclRorMinNeighbors = 4;
|
||||
};
|
||||
|
||||
struct TransformConfig
|
||||
{
|
||||
bool enabled = false;
|
||||
QString targetFrameId = "map";
|
||||
};
|
||||
|
||||
struct RegistrationConfig
|
||||
{
|
||||
bool enabled = false;
|
||||
QString method = "icp_centroid";
|
||||
int maxIterations = 20;
|
||||
};
|
||||
|
||||
struct ReconstructionConfig
|
||||
{
|
||||
float neighborRadiusScale = 3.5f;
|
||||
int runEveryNthFrame = 1;
|
||||
float pclGreedySearchRadius = 0.08f;
|
||||
float pclGreedyMu = 2.5f;
|
||||
int pclGreedyMaxNearest = 100;
|
||||
float pclGreedyMaxSurfaceAngle = 0.8f;
|
||||
};
|
||||
|
||||
struct RuntimeConfig
|
||||
{
|
||||
QString profile = "desktop_debug";
|
||||
bool asyncReconstruction = false;
|
||||
int maxQueueDepth = 4;
|
||||
int targetFps = 15;
|
||||
};
|
||||
|
||||
struct PipelineConfig
|
||||
{
|
||||
QStringList preprocessPlugins;
|
||||
QStringList transformPlugins;
|
||||
QStringList registrationPlugins;
|
||||
QString reconstructionPlugin = "surface_fallback";
|
||||
PreprocessConfig preprocess;
|
||||
TransformConfig transform;
|
||||
RegistrationConfig registration;
|
||||
ReconstructionConfig reconstruction;
|
||||
RuntimeConfig runtime;
|
||||
};
|
||||
|
||||
PipelineConfig makeDesktopDebugConfig();
|
||||
PipelineConfig makeRpi4RuntimeConfig();
|
||||
} // namespace core
|
||||
|
||||
#endif // PIPELINE_CONFIG_H
|
||||
@@ -0,0 +1,113 @@
|
||||
#include "pipeline_executor.h"
|
||||
|
||||
#include <QElapsedTimer>
|
||||
|
||||
namespace core
|
||||
{
|
||||
void PipelineExecutor::addPreprocessStage(const std::shared_ptr<IPreprocessStage> &stage)
|
||||
{
|
||||
if (!stage) {
|
||||
return;
|
||||
}
|
||||
m_preprocessStages.push_back(stage);
|
||||
}
|
||||
|
||||
void PipelineExecutor::addTransformStage(const std::shared_ptr<ITransformStage> &stage)
|
||||
{
|
||||
if (!stage) {
|
||||
return;
|
||||
}
|
||||
m_transformStages.push_back(stage);
|
||||
}
|
||||
|
||||
void PipelineExecutor::addRegistrationStage(const std::shared_ptr<IRegistrationStage> &stage)
|
||||
{
|
||||
if (!stage) {
|
||||
return;
|
||||
}
|
||||
m_registrationStages.push_back(stage);
|
||||
}
|
||||
|
||||
void PipelineExecutor::addLocalizationStage(const std::shared_ptr<ILocalizationStage> &stage)
|
||||
{
|
||||
if (!stage) {
|
||||
return;
|
||||
}
|
||||
m_localizationStages.push_back(stage);
|
||||
}
|
||||
|
||||
void PipelineExecutor::addSegmentationStage(const std::shared_ptr<ISegmentationStage> &stage)
|
||||
{
|
||||
if (!stage) {
|
||||
return;
|
||||
}
|
||||
m_segmentationStages.push_back(stage);
|
||||
}
|
||||
|
||||
void PipelineExecutor::addMappingStage(const std::shared_ptr<IMappingStage> &stage)
|
||||
{
|
||||
if (!stage) {
|
||||
return;
|
||||
}
|
||||
m_mappingStages.push_back(stage);
|
||||
}
|
||||
|
||||
void PipelineExecutor::addPlanningStage(const std::shared_ptr<IPlanningStage> &stage)
|
||||
{
|
||||
if (!stage) {
|
||||
return;
|
||||
}
|
||||
m_planningStages.push_back(stage);
|
||||
}
|
||||
|
||||
void PipelineExecutor::setReconstructionStage(const std::shared_ptr<IReconstructionStage> &stage)
|
||||
{
|
||||
m_reconstructionStage = stage;
|
||||
}
|
||||
|
||||
PipelineResult PipelineExecutor::run(const PointCloudFrame &input) const
|
||||
{
|
||||
PipelineResult result;
|
||||
result.stats.inputPoints = input.points.size();
|
||||
result.context.inputFrameId = input.frameId;
|
||||
|
||||
PointCloudFrame current = input;
|
||||
for (const std::shared_ptr<IPreprocessStage> &stage : m_preprocessStages) {
|
||||
current = stage->process(current, result.stats);
|
||||
}
|
||||
for (const std::shared_ptr<ITransformStage> &stage : m_transformStages) {
|
||||
current = stage->process(current, result.context, result.stats);
|
||||
}
|
||||
for (const std::shared_ptr<IRegistrationStage> &stage : m_registrationStages) {
|
||||
current = stage->process(current, result.context, result.stats);
|
||||
}
|
||||
for (const std::shared_ptr<ILocalizationStage> &stage : m_localizationStages) {
|
||||
stage->process(current, result.context, result.stats);
|
||||
}
|
||||
for (const std::shared_ptr<ISegmentationStage> &stage : m_segmentationStages) {
|
||||
stage->process(current, result.context, result.stats);
|
||||
}
|
||||
for (const std::shared_ptr<IMappingStage> &stage : m_mappingStages) {
|
||||
stage->process(current, result.context, result.stats);
|
||||
}
|
||||
for (const std::shared_ptr<IPlanningStage> &stage : m_planningStages) {
|
||||
stage->process(result.context, result.stats);
|
||||
}
|
||||
|
||||
result.stats.afterPreprocessingPoints = current.points.size();
|
||||
result.frame = current;
|
||||
|
||||
if (!m_reconstructionStage) {
|
||||
result.stats.outputTriangles = 0;
|
||||
result.stats.reconstructionMs = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
result.triangles = m_reconstructionStage->reconstruct(current, result.context, result.stats);
|
||||
result.stats.reconstructionMs = timer.elapsed();
|
||||
result.stats.outputTriangles = result.triangles.size();
|
||||
return result;
|
||||
}
|
||||
} // namespace core
|
||||
@@ -0,0 +1,35 @@
|
||||
#ifndef PIPELINE_EXECUTOR_H
|
||||
#define PIPELINE_EXECUTOR_H
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "pipeline_stage.h"
|
||||
|
||||
namespace core
|
||||
{
|
||||
class PipelineExecutor
|
||||
{
|
||||
public:
|
||||
void addPreprocessStage(const std::shared_ptr<IPreprocessStage> &stage);
|
||||
void addTransformStage(const std::shared_ptr<ITransformStage> &stage);
|
||||
void addRegistrationStage(const std::shared_ptr<IRegistrationStage> &stage);
|
||||
void addLocalizationStage(const std::shared_ptr<ILocalizationStage> &stage);
|
||||
void addSegmentationStage(const std::shared_ptr<ISegmentationStage> &stage);
|
||||
void addMappingStage(const std::shared_ptr<IMappingStage> &stage);
|
||||
void addPlanningStage(const std::shared_ptr<IPlanningStage> &stage);
|
||||
void setReconstructionStage(const std::shared_ptr<IReconstructionStage> &stage);
|
||||
PipelineResult run(const PointCloudFrame &input) const;
|
||||
|
||||
private:
|
||||
QVector<std::shared_ptr<IPreprocessStage> > m_preprocessStages;
|
||||
QVector<std::shared_ptr<ITransformStage> > m_transformStages;
|
||||
QVector<std::shared_ptr<IRegistrationStage> > m_registrationStages;
|
||||
QVector<std::shared_ptr<ILocalizationStage> > m_localizationStages;
|
||||
QVector<std::shared_ptr<ISegmentationStage> > m_segmentationStages;
|
||||
QVector<std::shared_ptr<IMappingStage> > m_mappingStages;
|
||||
QVector<std::shared_ptr<IPlanningStage> > m_planningStages;
|
||||
std::shared_ptr<IReconstructionStage> m_reconstructionStage;
|
||||
};
|
||||
} // namespace core
|
||||
|
||||
#endif // PIPELINE_EXECUTOR_H
|
||||
@@ -0,0 +1,52 @@
|
||||
#include "pipeline_plugin_registry.h"
|
||||
|
||||
namespace core
|
||||
{
|
||||
void PipelinePluginRegistry::registerPreprocess(const QString &id, const PreprocessFactory &factory)
|
||||
{
|
||||
m_preprocessFactories.insert(id, factory);
|
||||
}
|
||||
|
||||
void PipelinePluginRegistry::registerTransform(const QString &id, const TransformFactory &factory)
|
||||
{
|
||||
m_transformFactories.insert(id, factory);
|
||||
}
|
||||
|
||||
void PipelinePluginRegistry::registerRegistration(const QString &id, const RegistrationFactory &factory)
|
||||
{
|
||||
m_registrationFactories.insert(id, factory);
|
||||
}
|
||||
|
||||
void PipelinePluginRegistry::registerReconstruction(const QString &id, const ReconstructionFactory &factory)
|
||||
{
|
||||
m_reconstructionFactories.insert(id, factory);
|
||||
}
|
||||
|
||||
std::shared_ptr<IPreprocessStage> PipelinePluginRegistry::createPreprocess(
|
||||
const QString &id,
|
||||
const PipelineConfig &config) const
|
||||
{
|
||||
return m_preprocessFactories.contains(id) ? m_preprocessFactories.value(id)(config) : std::shared_ptr<IPreprocessStage>();
|
||||
}
|
||||
|
||||
std::shared_ptr<ITransformStage> PipelinePluginRegistry::createTransform(
|
||||
const QString &id,
|
||||
const PipelineConfig &config) const
|
||||
{
|
||||
return m_transformFactories.contains(id) ? m_transformFactories.value(id)(config) : std::shared_ptr<ITransformStage>();
|
||||
}
|
||||
|
||||
std::shared_ptr<IRegistrationStage> PipelinePluginRegistry::createRegistration(
|
||||
const QString &id,
|
||||
const PipelineConfig &config) const
|
||||
{
|
||||
return m_registrationFactories.contains(id) ? m_registrationFactories.value(id)(config) : std::shared_ptr<IRegistrationStage>();
|
||||
}
|
||||
|
||||
std::shared_ptr<IReconstructionStage> PipelinePluginRegistry::createReconstruction(
|
||||
const QString &id,
|
||||
const PipelineConfig &config) const
|
||||
{
|
||||
return m_reconstructionFactories.contains(id) ? m_reconstructionFactories.value(id)(config) : std::shared_ptr<IReconstructionStage>();
|
||||
}
|
||||
} // namespace core
|
||||
@@ -0,0 +1,40 @@
|
||||
#ifndef PIPELINE_PLUGIN_REGISTRY_H
|
||||
#define PIPELINE_PLUGIN_REGISTRY_H
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
#include <QHash>
|
||||
|
||||
#include "pipeline_config.h"
|
||||
#include "pipeline_stage.h"
|
||||
|
||||
namespace core
|
||||
{
|
||||
class PipelinePluginRegistry
|
||||
{
|
||||
public:
|
||||
typedef std::function<std::shared_ptr<IPreprocessStage>(const PipelineConfig &)> PreprocessFactory;
|
||||
typedef std::function<std::shared_ptr<ITransformStage>(const PipelineConfig &)> TransformFactory;
|
||||
typedef std::function<std::shared_ptr<IRegistrationStage>(const PipelineConfig &)> RegistrationFactory;
|
||||
typedef std::function<std::shared_ptr<IReconstructionStage>(const PipelineConfig &)> ReconstructionFactory;
|
||||
|
||||
void registerPreprocess(const QString &id, const PreprocessFactory &factory);
|
||||
void registerTransform(const QString &id, const TransformFactory &factory);
|
||||
void registerRegistration(const QString &id, const RegistrationFactory &factory);
|
||||
void registerReconstruction(const QString &id, const ReconstructionFactory &factory);
|
||||
|
||||
std::shared_ptr<IPreprocessStage> createPreprocess(const QString &id, const PipelineConfig &config) const;
|
||||
std::shared_ptr<ITransformStage> createTransform(const QString &id, const PipelineConfig &config) const;
|
||||
std::shared_ptr<IRegistrationStage> createRegistration(const QString &id, const PipelineConfig &config) const;
|
||||
std::shared_ptr<IReconstructionStage> createReconstruction(const QString &id, const PipelineConfig &config) const;
|
||||
|
||||
private:
|
||||
QHash<QString, PreprocessFactory> m_preprocessFactories;
|
||||
QHash<QString, TransformFactory> m_transformFactories;
|
||||
QHash<QString, RegistrationFactory> m_registrationFactories;
|
||||
QHash<QString, ReconstructionFactory> m_reconstructionFactories;
|
||||
};
|
||||
} // namespace core
|
||||
|
||||
#endif // PIPELINE_PLUGIN_REGISTRY_H
|
||||
@@ -0,0 +1,83 @@
|
||||
#ifndef PIPELINE_STAGE_H
|
||||
#define PIPELINE_STAGE_H
|
||||
|
||||
#include "point_cloud_types.h"
|
||||
|
||||
namespace core
|
||||
{
|
||||
class IPreprocessStage
|
||||
{
|
||||
public:
|
||||
virtual ~IPreprocessStage() {}
|
||||
virtual PointCloudFrame process(const PointCloudFrame &input, PipelineStats &stats) const = 0;
|
||||
};
|
||||
|
||||
class ITransformStage
|
||||
{
|
||||
public:
|
||||
virtual ~ITransformStage() {}
|
||||
virtual PointCloudFrame process(
|
||||
const PointCloudFrame &input,
|
||||
PipelineContext &context,
|
||||
PipelineStats &stats) const = 0;
|
||||
};
|
||||
|
||||
class IRegistrationStage
|
||||
{
|
||||
public:
|
||||
virtual ~IRegistrationStage() {}
|
||||
virtual PointCloudFrame process(
|
||||
const PointCloudFrame &input,
|
||||
PipelineContext &context,
|
||||
PipelineStats &stats) const = 0;
|
||||
};
|
||||
|
||||
class ILocalizationStage
|
||||
{
|
||||
public:
|
||||
virtual ~ILocalizationStage() {}
|
||||
virtual void process(
|
||||
const PointCloudFrame &input,
|
||||
PipelineContext &context,
|
||||
PipelineStats &stats) const = 0;
|
||||
};
|
||||
|
||||
class ISegmentationStage
|
||||
{
|
||||
public:
|
||||
virtual ~ISegmentationStage() {}
|
||||
virtual void process(
|
||||
const PointCloudFrame &input,
|
||||
PipelineContext &context,
|
||||
PipelineStats &stats) const = 0;
|
||||
};
|
||||
|
||||
class IMappingStage
|
||||
{
|
||||
public:
|
||||
virtual ~IMappingStage() {}
|
||||
virtual void process(
|
||||
const PointCloudFrame &input,
|
||||
PipelineContext &context,
|
||||
PipelineStats &stats) const = 0;
|
||||
};
|
||||
|
||||
class IPlanningStage
|
||||
{
|
||||
public:
|
||||
virtual ~IPlanningStage() {}
|
||||
virtual void process(const PipelineContext &context, PipelineStats &stats) const = 0;
|
||||
};
|
||||
|
||||
class IReconstructionStage
|
||||
{
|
||||
public:
|
||||
virtual ~IReconstructionStage() {}
|
||||
virtual QVector<Triangle> reconstruct(
|
||||
const PointCloudFrame &frame,
|
||||
const PipelineContext &context,
|
||||
PipelineStats &stats) const = 0;
|
||||
};
|
||||
} // namespace core
|
||||
|
||||
#endif // PIPELINE_STAGE_H
|
||||
@@ -0,0 +1,78 @@
|
||||
#ifndef POINT_CLOUD_TYPES_H
|
||||
#define POINT_CLOUD_TYPES_H
|
||||
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
namespace core
|
||||
{
|
||||
struct Point3f
|
||||
{
|
||||
float x;
|
||||
float y;
|
||||
float z;
|
||||
};
|
||||
|
||||
struct Triangle
|
||||
{
|
||||
int i0;
|
||||
int i1;
|
||||
int i2;
|
||||
};
|
||||
|
||||
struct PointCloudFrame
|
||||
{
|
||||
QVector<Point3f> points;
|
||||
QString sourceLabel;
|
||||
QString frameId;
|
||||
qint64 timestampUsec = 0;
|
||||
};
|
||||
|
||||
struct PoseEstimate
|
||||
{
|
||||
float tx = 0.0f;
|
||||
float ty = 0.0f;
|
||||
float tz = 0.0f;
|
||||
float qx = 0.0f;
|
||||
float qy = 0.0f;
|
||||
float qz = 0.0f;
|
||||
float qw = 1.0f;
|
||||
};
|
||||
|
||||
struct PipelineContext
|
||||
{
|
||||
QString inputFrameId;
|
||||
QString targetFrameId;
|
||||
bool hasPoseEstimate = false;
|
||||
PoseEstimate poseEstimate;
|
||||
double registrationFitness = 0.0;
|
||||
double registrationRmse = 0.0;
|
||||
qint64 registrationMs = 0;
|
||||
qint64 transformMs = 0;
|
||||
qint64 localizationMs = 0;
|
||||
qint64 segmentationMs = 0;
|
||||
qint64 mappingMs = 0;
|
||||
qint64 planningMs = 0;
|
||||
};
|
||||
|
||||
struct PipelineStats
|
||||
{
|
||||
int inputPoints = 0;
|
||||
int afterPreprocessingPoints = 0;
|
||||
int outputTriangles = 0;
|
||||
int removedClusterPoints = 0;
|
||||
int removedDownsamplePoints = 0;
|
||||
int detectedClusters = 1;
|
||||
qint64 reconstructionMs = 0;
|
||||
};
|
||||
|
||||
struct PipelineResult
|
||||
{
|
||||
PointCloudFrame frame;
|
||||
QVector<Triangle> triangles;
|
||||
PipelineStats stats;
|
||||
PipelineContext context;
|
||||
};
|
||||
} // namespace core
|
||||
|
||||
#endif // POINT_CLOUD_TYPES_H
|
||||
@@ -0,0 +1,94 @@
|
||||
#include "desktop_pipeline_factory.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "../../strategies/preprocess_basic_stages.h"
|
||||
#include "../../strategies/preprocess_pcl_stages.h"
|
||||
#include "../../strategies/registration_icp_stage.h"
|
||||
#include "../../strategies/reconstruction_pcl_greedy_stage.h"
|
||||
#include "../../strategies/reconstruction_surface_stage.h"
|
||||
#include "../../strategies/transform_tf_stage.h"
|
||||
|
||||
namespace factories
|
||||
{
|
||||
namespace pipeline
|
||||
{
|
||||
core::PipelineExecutor createDesktopPipelineExecutor(const core::PipelineConfig &config)
|
||||
{
|
||||
const core::PipelinePluginRegistry registry = createDefaultPluginRegistry();
|
||||
core::PipelineExecutor executor;
|
||||
|
||||
const QStringList preprocessIds = config.preprocessPlugins.isEmpty()
|
||||
? (QStringList() << "keep_largest_cluster" << "downsample_dense")
|
||||
: config.preprocessPlugins;
|
||||
|
||||
for (const QString &id : preprocessIds) {
|
||||
const std::shared_ptr<core::IPreprocessStage> stage = registry.createPreprocess(id, config);
|
||||
if (stage) {
|
||||
executor.addPreprocessStage(stage);
|
||||
}
|
||||
}
|
||||
|
||||
for (const QString &id : config.transformPlugins) {
|
||||
const std::shared_ptr<core::ITransformStage> stage = registry.createTransform(id, config);
|
||||
if (stage) {
|
||||
executor.addTransformStage(stage);
|
||||
}
|
||||
}
|
||||
|
||||
for (const QString &id : config.registrationPlugins) {
|
||||
const std::shared_ptr<core::IRegistrationStage> stage = registry.createRegistration(id, config);
|
||||
if (stage) {
|
||||
executor.addRegistrationStage(stage);
|
||||
}
|
||||
}
|
||||
|
||||
const std::shared_ptr<core::IReconstructionStage> reconstruction = registry.createReconstruction(
|
||||
config.reconstructionPlugin,
|
||||
config);
|
||||
executor.setReconstructionStage(reconstruction);
|
||||
return executor;
|
||||
}
|
||||
|
||||
core::PipelineExecutor createPipelineExecutorForProfile(const QString &profileName)
|
||||
{
|
||||
if (profileName == "rpi4_runtime") {
|
||||
return createDesktopPipelineExecutor(core::makeRpi4RuntimeConfig());
|
||||
}
|
||||
return createDesktopPipelineExecutor(core::makeDesktopDebugConfig());
|
||||
}
|
||||
|
||||
core::PipelinePluginRegistry createDefaultPluginRegistry()
|
||||
{
|
||||
core::PipelinePluginRegistry registry;
|
||||
registry.registerPreprocess("keep_largest_cluster", [](const core::PipelineConfig &config) {
|
||||
return std::shared_ptr<core::IPreprocessStage>(new strategies::KeepLargestClusterStage(config.preprocess));
|
||||
});
|
||||
registry.registerPreprocess("downsample_dense", [](const core::PipelineConfig &config) {
|
||||
return std::shared_ptr<core::IPreprocessStage>(new strategies::DownsampleDenseAreasStage(config.preprocess));
|
||||
});
|
||||
registry.registerPreprocess("pcl_voxel_grid", [](const core::PipelineConfig &config) {
|
||||
return std::shared_ptr<core::IPreprocessStage>(new strategies::PclVoxelGridStage(config.preprocess));
|
||||
});
|
||||
registry.registerPreprocess("pcl_statistical_outlier", [](const core::PipelineConfig &config) {
|
||||
return std::shared_ptr<core::IPreprocessStage>(new strategies::PclStatisticalOutlierStage(config.preprocess));
|
||||
});
|
||||
registry.registerPreprocess("pcl_radius_outlier", [](const core::PipelineConfig &config) {
|
||||
return std::shared_ptr<core::IPreprocessStage>(new strategies::PclRadiusOutlierStage(config.preprocess));
|
||||
});
|
||||
registry.registerTransform("tf_transform", [](const core::PipelineConfig &config) {
|
||||
return std::shared_ptr<core::ITransformStage>(new strategies::TransformTfStage(config));
|
||||
});
|
||||
registry.registerRegistration("icp_centroid", [](const core::PipelineConfig &config) {
|
||||
return std::shared_ptr<core::IRegistrationStage>(new strategies::RegistrationIcpStage(config));
|
||||
});
|
||||
registry.registerReconstruction("surface_fallback", [](const core::PipelineConfig &config) {
|
||||
return std::shared_ptr<core::IReconstructionStage>(new strategies::SurfaceReconstructionStage(config.reconstruction));
|
||||
});
|
||||
registry.registerReconstruction("pcl_greedy_triangulation", [](const core::PipelineConfig &config) {
|
||||
return std::shared_ptr<core::IReconstructionStage>(new strategies::PclGreedyReconstructionStage(config.reconstruction));
|
||||
});
|
||||
return registry;
|
||||
}
|
||||
} // namespace pipeline
|
||||
} // namespace factories
|
||||
@@ -0,0 +1,18 @@
|
||||
#ifndef DESKTOP_PIPELINE_FACTORY_H
|
||||
#define DESKTOP_PIPELINE_FACTORY_H
|
||||
|
||||
#include "../../core/pipeline_config.h"
|
||||
#include "../../core/pipeline_executor.h"
|
||||
#include "../../core/pipeline_plugin_registry.h"
|
||||
|
||||
namespace factories
|
||||
{
|
||||
namespace pipeline
|
||||
{
|
||||
core::PipelineExecutor createDesktopPipelineExecutor(const core::PipelineConfig &config = core::PipelineConfig());
|
||||
core::PipelineExecutor createPipelineExecutorForProfile(const QString &profileName);
|
||||
core::PipelinePluginRegistry createDefaultPluginRegistry();
|
||||
} // namespace pipeline
|
||||
} // namespace factories
|
||||
|
||||
#endif // DESKTOP_PIPELINE_FACTORY_H
|
||||
@@ -0,0 +1,36 @@
|
||||
#include "preprocess_basic_stages.h"
|
||||
#include "../algorithms/preprocess/preprocess_algorithms.h"
|
||||
|
||||
namespace strategies
|
||||
{
|
||||
KeepLargestClusterStage::KeepLargestClusterStage(const core::PreprocessConfig &config)
|
||||
: m_config(config)
|
||||
{
|
||||
}
|
||||
|
||||
core::PointCloudFrame KeepLargestClusterStage::process(const core::PointCloudFrame &input, core::PipelineStats &stats) const
|
||||
{
|
||||
core::PointCloudFrame output = input;
|
||||
output.points = algorithms::preprocess::keepLargestCluster(
|
||||
input.points,
|
||||
stats.removedClusterPoints,
|
||||
stats.detectedClusters,
|
||||
m_config.clusterJoinDistanceScale);
|
||||
return output;
|
||||
}
|
||||
|
||||
DownsampleDenseAreasStage::DownsampleDenseAreasStage(const core::PreprocessConfig &config)
|
||||
: m_config(config)
|
||||
{
|
||||
}
|
||||
|
||||
core::PointCloudFrame DownsampleDenseAreasStage::process(const core::PointCloudFrame &input, core::PipelineStats &stats) const
|
||||
{
|
||||
core::PointCloudFrame output = input;
|
||||
output.points = algorithms::preprocess::downsampleDenseAreas(
|
||||
input.points,
|
||||
stats.removedDownsamplePoints,
|
||||
m_config.downsampleCellScale);
|
||||
return output;
|
||||
}
|
||||
} // namespace strategies
|
||||
@@ -0,0 +1,30 @@
|
||||
#ifndef PREPROCESS_BASIC_STAGES_H
|
||||
#define PREPROCESS_BASIC_STAGES_H
|
||||
|
||||
#include "../core/pipeline_config.h"
|
||||
#include "../core/pipeline_stage.h"
|
||||
|
||||
namespace strategies
|
||||
{
|
||||
class KeepLargestClusterStage : public core::IPreprocessStage
|
||||
{
|
||||
public:
|
||||
explicit KeepLargestClusterStage(const core::PreprocessConfig &config = core::PreprocessConfig());
|
||||
core::PointCloudFrame process(const core::PointCloudFrame &input, core::PipelineStats &stats) const override;
|
||||
|
||||
private:
|
||||
core::PreprocessConfig m_config;
|
||||
};
|
||||
|
||||
class DownsampleDenseAreasStage : public core::IPreprocessStage
|
||||
{
|
||||
public:
|
||||
explicit DownsampleDenseAreasStage(const core::PreprocessConfig &config = core::PreprocessConfig());
|
||||
core::PointCloudFrame process(const core::PointCloudFrame &input, core::PipelineStats &stats) const override;
|
||||
|
||||
private:
|
||||
core::PreprocessConfig m_config;
|
||||
};
|
||||
} // namespace strategies
|
||||
|
||||
#endif // PREPROCESS_BASIC_STAGES_H
|
||||
@@ -0,0 +1,53 @@
|
||||
#include "preprocess_pcl_stages.h"
|
||||
|
||||
#include "../adapters/pcl/pcl_point_cloud_adapter.h"
|
||||
#include "../algorithms/preprocess/preprocess_algorithms.h"
|
||||
|
||||
namespace strategies
|
||||
{
|
||||
core::PointCloudFrame PclVoxelGridStage::process(const core::PointCloudFrame &input, core::PipelineStats &stats) const
|
||||
{
|
||||
core::PointCloudFrame output = input;
|
||||
int removed = 0;
|
||||
output.points = adapters::pcl::applyVoxelGrid(input.points, m_config.pclVoxelLeafSize, removed);
|
||||
#ifndef PCL_ENABLED
|
||||
output.points = algorithms::preprocess::downsampleDenseAreas(input.points, removed, m_config.downsampleCellScale);
|
||||
#endif
|
||||
stats.removedDownsamplePoints += removed;
|
||||
return output;
|
||||
}
|
||||
|
||||
core::PointCloudFrame PclStatisticalOutlierStage::process(const core::PointCloudFrame &input, core::PipelineStats &stats) const
|
||||
{
|
||||
core::PointCloudFrame output = input;
|
||||
int removed = 0;
|
||||
int clusters = 1;
|
||||
output.points = adapters::pcl::applyStatisticalOutlierRemoval(
|
||||
input.points,
|
||||
m_config.pclSorMeanK,
|
||||
m_config.pclSorStdDevMul,
|
||||
removed);
|
||||
#ifndef PCL_ENABLED
|
||||
output.points = algorithms::preprocess::keepLargestCluster(input.points, removed, clusters, m_config.clusterJoinDistanceScale);
|
||||
#endif
|
||||
stats.removedClusterPoints += removed;
|
||||
stats.detectedClusters = clusters;
|
||||
return output;
|
||||
}
|
||||
|
||||
core::PointCloudFrame PclRadiusOutlierStage::process(const core::PointCloudFrame &input, core::PipelineStats &stats) const
|
||||
{
|
||||
core::PointCloudFrame output = input;
|
||||
int removed = 0;
|
||||
output.points = adapters::pcl::applyRadiusOutlierRemoval(
|
||||
input.points,
|
||||
m_config.pclRorRadius,
|
||||
m_config.pclRorMinNeighbors,
|
||||
removed);
|
||||
#ifndef PCL_ENABLED
|
||||
output.points = algorithms::preprocess::downsampleDenseAreas(input.points, removed, m_config.downsampleCellScale * 0.8);
|
||||
#endif
|
||||
stats.removedDownsamplePoints += removed;
|
||||
return output;
|
||||
}
|
||||
} // namespace strategies
|
||||
@@ -0,0 +1,52 @@
|
||||
#ifndef PREPROCESS_PCL_STAGES_H
|
||||
#define PREPROCESS_PCL_STAGES_H
|
||||
|
||||
#include "../core/pipeline_config.h"
|
||||
#include "../core/pipeline_stage.h"
|
||||
|
||||
namespace strategies
|
||||
{
|
||||
class PclVoxelGridStage : public core::IPreprocessStage
|
||||
{
|
||||
public:
|
||||
explicit PclVoxelGridStage(const core::PreprocessConfig &config)
|
||||
: m_config(config)
|
||||
{
|
||||
}
|
||||
|
||||
core::PointCloudFrame process(const core::PointCloudFrame &input, core::PipelineStats &stats) const override;
|
||||
|
||||
private:
|
||||
core::PreprocessConfig m_config;
|
||||
};
|
||||
|
||||
class PclStatisticalOutlierStage : public core::IPreprocessStage
|
||||
{
|
||||
public:
|
||||
explicit PclStatisticalOutlierStage(const core::PreprocessConfig &config)
|
||||
: m_config(config)
|
||||
{
|
||||
}
|
||||
|
||||
core::PointCloudFrame process(const core::PointCloudFrame &input, core::PipelineStats &stats) const override;
|
||||
|
||||
private:
|
||||
core::PreprocessConfig m_config;
|
||||
};
|
||||
|
||||
class PclRadiusOutlierStage : public core::IPreprocessStage
|
||||
{
|
||||
public:
|
||||
explicit PclRadiusOutlierStage(const core::PreprocessConfig &config)
|
||||
: m_config(config)
|
||||
{
|
||||
}
|
||||
|
||||
core::PointCloudFrame process(const core::PointCloudFrame &input, core::PipelineStats &stats) const override;
|
||||
|
||||
private:
|
||||
core::PreprocessConfig m_config;
|
||||
};
|
||||
} // namespace strategies
|
||||
|
||||
#endif // PREPROCESS_PCL_STAGES_H
|
||||
@@ -0,0 +1,41 @@
|
||||
#ifndef RECONSTRUCTION_PCL_GREEDY_STAGE_H
|
||||
#define RECONSTRUCTION_PCL_GREEDY_STAGE_H
|
||||
|
||||
#include "../adapters/pcl/pcl_point_cloud_adapter.h"
|
||||
#include "../adapters/reconstruction/reconstruction_adapter.h"
|
||||
#include "../core/pipeline_config.h"
|
||||
#include "../core/pipeline_stage.h"
|
||||
|
||||
namespace strategies
|
||||
{
|
||||
class PclGreedyReconstructionStage : public core::IReconstructionStage
|
||||
{
|
||||
public:
|
||||
explicit PclGreedyReconstructionStage(const core::ReconstructionConfig &config)
|
||||
: m_config(config)
|
||||
{
|
||||
}
|
||||
|
||||
QVector<core::Triangle> reconstruct(
|
||||
const core::PointCloudFrame &frame,
|
||||
const core::PipelineContext &,
|
||||
core::PipelineStats &) const override
|
||||
{
|
||||
QVector<core::Triangle> triangles = adapters::pcl::buildGreedyTriangles(
|
||||
frame.points,
|
||||
m_config.pclGreedySearchRadius,
|
||||
m_config.pclGreedyMu,
|
||||
m_config.pclGreedyMaxNearest,
|
||||
m_config.pclGreedyMaxSurfaceAngle);
|
||||
if (!triangles.isEmpty()) {
|
||||
return triangles;
|
||||
}
|
||||
return adapters::reconstruction::buildSurfaceTriangles(frame.points, m_config);
|
||||
}
|
||||
|
||||
private:
|
||||
core::ReconstructionConfig m_config;
|
||||
};
|
||||
} // namespace strategies
|
||||
|
||||
#endif // RECONSTRUCTION_PCL_GREEDY_STAGE_H
|
||||
@@ -0,0 +1,31 @@
|
||||
#ifndef RECONSTRUCTION_SURFACE_STAGE_H
|
||||
#define RECONSTRUCTION_SURFACE_STAGE_H
|
||||
|
||||
#include "../adapters/reconstruction/reconstruction_adapter.h"
|
||||
#include "../core/pipeline_config.h"
|
||||
#include "../core/pipeline_stage.h"
|
||||
|
||||
namespace strategies
|
||||
{
|
||||
class SurfaceReconstructionStage : public core::IReconstructionStage
|
||||
{
|
||||
public:
|
||||
explicit SurfaceReconstructionStage(const core::ReconstructionConfig &config = core::ReconstructionConfig())
|
||||
: m_config(config)
|
||||
{
|
||||
}
|
||||
|
||||
QVector<core::Triangle> reconstruct(
|
||||
const core::PointCloudFrame &frame,
|
||||
const core::PipelineContext &,
|
||||
core::PipelineStats &) const override
|
||||
{
|
||||
return adapters::reconstruction::buildSurfaceTriangles(frame.points, m_config);
|
||||
}
|
||||
|
||||
private:
|
||||
core::ReconstructionConfig m_config;
|
||||
};
|
||||
} // namespace strategies
|
||||
|
||||
#endif // RECONSTRUCTION_SURFACE_STAGE_H
|
||||
@@ -0,0 +1,30 @@
|
||||
#include "registration_icp_stage.h"
|
||||
|
||||
#include "../adapters/registration/registration_adapter.h"
|
||||
|
||||
#include <QElapsedTimer>
|
||||
|
||||
namespace strategies
|
||||
{
|
||||
core::PointCloudFrame RegistrationIcpStage::process(
|
||||
const core::PointCloudFrame &input,
|
||||
core::PipelineContext &context,
|
||||
core::PipelineStats &) const
|
||||
{
|
||||
if (!m_config.registration.enabled) {
|
||||
return input;
|
||||
}
|
||||
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
|
||||
const adapters::registration::RegistrationResult regResult =
|
||||
adapters::registration::alignToReference(input, m_config.registration);
|
||||
|
||||
context.hasPoseEstimate = true;
|
||||
context.registrationFitness = regResult.fitness;
|
||||
context.registrationRmse = regResult.rmse;
|
||||
context.registrationMs += timer.elapsed();
|
||||
return regResult.alignedFrame;
|
||||
}
|
||||
} // namespace strategies
|
||||
@@ -0,0 +1,27 @@
|
||||
#ifndef REGISTRATION_ICP_STAGE_H
|
||||
#define REGISTRATION_ICP_STAGE_H
|
||||
|
||||
#include "../core/pipeline_config.h"
|
||||
#include "../core/pipeline_stage.h"
|
||||
|
||||
namespace strategies
|
||||
{
|
||||
class RegistrationIcpStage : public core::IRegistrationStage
|
||||
{
|
||||
public:
|
||||
explicit RegistrationIcpStage(const core::PipelineConfig &config)
|
||||
: m_config(config)
|
||||
{
|
||||
}
|
||||
|
||||
core::PointCloudFrame process(
|
||||
const core::PointCloudFrame &input,
|
||||
core::PipelineContext &context,
|
||||
core::PipelineStats &stats) const override;
|
||||
|
||||
private:
|
||||
core::PipelineConfig m_config;
|
||||
};
|
||||
} // namespace strategies
|
||||
|
||||
#endif // REGISTRATION_ICP_STAGE_H
|
||||
@@ -0,0 +1,28 @@
|
||||
#include "transform_tf_stage.h"
|
||||
|
||||
#include <QElapsedTimer>
|
||||
|
||||
namespace strategies
|
||||
{
|
||||
core::PointCloudFrame TransformTfStage::process(
|
||||
const core::PointCloudFrame &input,
|
||||
core::PipelineContext &context,
|
||||
core::PipelineStats &) const
|
||||
{
|
||||
if (!m_config.transform.enabled) {
|
||||
return input;
|
||||
}
|
||||
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
|
||||
adapters::ros2::TfTransform transform;
|
||||
transform.fromFrame = input.frameId;
|
||||
transform.toFrame = m_config.transform.targetFrameId;
|
||||
const core::PointCloudFrame output = adapters::ros2::transformFrame(input, transform);
|
||||
|
||||
context.targetFrameId = output.frameId;
|
||||
context.transformMs += timer.elapsed();
|
||||
return output;
|
||||
}
|
||||
} // namespace strategies
|
||||
@@ -0,0 +1,28 @@
|
||||
#ifndef TRANSFORM_TF_STAGE_H
|
||||
#define TRANSFORM_TF_STAGE_H
|
||||
|
||||
#include "../adapters/ros2/tf2_adapter.h"
|
||||
#include "../core/pipeline_config.h"
|
||||
#include "../core/pipeline_stage.h"
|
||||
|
||||
namespace strategies
|
||||
{
|
||||
class TransformTfStage : public core::ITransformStage
|
||||
{
|
||||
public:
|
||||
explicit TransformTfStage(const core::PipelineConfig &config)
|
||||
: m_config(config)
|
||||
{
|
||||
}
|
||||
|
||||
core::PointCloudFrame process(
|
||||
const core::PointCloudFrame &input,
|
||||
core::PipelineContext &context,
|
||||
core::PipelineStats &) const override;
|
||||
|
||||
private:
|
||||
core::PipelineConfig m_config;
|
||||
};
|
||||
} // namespace strategies
|
||||
|
||||
#endif // TRANSFORM_TF_STAGE_H
|
||||
@@ -0,0 +1,196 @@
|
||||
#include "pipeline_smoke_tests.h"
|
||||
|
||||
#include "../algorithms/reconstruction/surface_reconstruction.h"
|
||||
#include "../adapters/sources/ros2_point_cloud_source.h"
|
||||
#include "../core/pipeline_config.h"
|
||||
#include "../core/pipeline_executor.h"
|
||||
#include "../factories/pipeline/desktop_pipeline_factory.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
QVector<core::Point3f> buildDemoCloud()
|
||||
{
|
||||
return QVector<core::Point3f>{
|
||||
{-1.0f, -1.0f, 0.0f},
|
||||
{1.0f, -1.0f, 0.0f},
|
||||
{1.0f, 1.0f, 0.0f},
|
||||
{-1.0f, 1.0f, 0.0f},
|
||||
{0.0f, 0.0f, 1.0f},
|
||||
{0.0f, 0.0f, -1.0f}
|
||||
};
|
||||
}
|
||||
|
||||
bool runDemoCloudCase(QString &failureReason)
|
||||
{
|
||||
core::PipelineExecutor executor = factories::pipeline::createDesktopPipelineExecutor();
|
||||
core::PointCloudFrame frame;
|
||||
frame.points = buildDemoCloud();
|
||||
frame.sourceLabel = "smoke-demo";
|
||||
const core::PipelineResult result = executor.run(frame);
|
||||
if (result.triangles.isEmpty()) {
|
||||
failureReason = "Demo cloud produced zero triangles.";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool runValidCloudCase(QString &failureReason)
|
||||
{
|
||||
core::PipelineExecutor executor = factories::pipeline::createDesktopPipelineExecutor();
|
||||
core::PointCloudFrame frame;
|
||||
frame.points = buildDemoCloud();
|
||||
frame.points.push_back({2.0f, 0.0f, 0.3f});
|
||||
frame.sourceLabel = "smoke-valid";
|
||||
const core::PipelineResult result = executor.run(frame);
|
||||
if (result.stats.inputPoints < 4 || result.stats.afterPreprocessingPoints < 4) {
|
||||
failureReason = "Pipeline dropped points unexpectedly.";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool runNeighborRadiusCase(QString &failureReason)
|
||||
{
|
||||
const float oldScale = algorithms::reconstruction::neighborRadiusScale();
|
||||
algorithms::reconstruction::setNeighborRadiusScale(2.0f);
|
||||
|
||||
core::PipelineConfig config;
|
||||
config.reconstruction.neighborRadiusScale = 2.0f;
|
||||
core::PipelineExecutor executor = factories::pipeline::createDesktopPipelineExecutor(config);
|
||||
|
||||
core::PointCloudFrame frame;
|
||||
frame.points = buildDemoCloud();
|
||||
frame.sourceLabel = "smoke-radius";
|
||||
const core::PipelineResult result = executor.run(frame);
|
||||
|
||||
algorithms::reconstruction::setNeighborRadiusScale(oldScale);
|
||||
|
||||
if (result.triangles.isEmpty()) {
|
||||
failureReason = "Neighbor radius run produced zero triangles.";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool runRos2MockSourceCase(QString &failureReason)
|
||||
{
|
||||
adapters::sources::Ros2PointCloudSource source;
|
||||
adapters::ros2::PointCloud2Message msg;
|
||||
msg.frameId = "lidar";
|
||||
msg.timestampUsec = 42;
|
||||
msg.xyz = buildDemoCloud();
|
||||
source.pushMockMessage(msg);
|
||||
|
||||
core::PointCloudFrame frame;
|
||||
if (!source.nextFrame(frame, failureReason)) {
|
||||
return false;
|
||||
}
|
||||
if (frame.frameId != "lidar" || frame.points.size() != msg.xyz.size()) {
|
||||
failureReason = "ROS2 mock conversion mismatch.";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool runRpiProfileCase(QString &failureReason)
|
||||
{
|
||||
core::PipelineConfig config = core::makeRpi4RuntimeConfig();
|
||||
config.transform.enabled = true;
|
||||
config.transformPlugins = QStringList() << "tf_transform";
|
||||
config.registration.enabled = true;
|
||||
config.registrationPlugins = QStringList() << "icp_centroid";
|
||||
core::PipelineExecutor executor = factories::pipeline::createDesktopPipelineExecutor(config);
|
||||
|
||||
core::PointCloudFrame frame;
|
||||
frame.points = buildDemoCloud();
|
||||
frame.frameId = "sensor";
|
||||
frame.sourceLabel = "smoke-rpi4";
|
||||
const core::PipelineResult result = executor.run(frame);
|
||||
if (!result.context.hasPoseEstimate) {
|
||||
failureReason = "Registration stage did not populate pose estimate.";
|
||||
return false;
|
||||
}
|
||||
if (result.frame.frameId != "map") {
|
||||
failureReason = "TF stage did not set target frame.";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool runPluginChainCase(QString &failureReason)
|
||||
{
|
||||
core::PipelineConfig config = core::makeDesktopDebugConfig();
|
||||
config.preprocessPlugins = QStringList() << "pcl_voxel_grid" << "pcl_statistical_outlier" << "pcl_radius_outlier";
|
||||
config.reconstructionPlugin = "surface_fallback";
|
||||
config.preprocess.pclSorMeanK = 4;
|
||||
config.preprocess.pclRorRadius = 0.5f;
|
||||
config.preprocess.pclRorMinNeighbors = 2;
|
||||
core::PipelineExecutor executor = factories::pipeline::createDesktopPipelineExecutor(config);
|
||||
|
||||
core::PointCloudFrame frame;
|
||||
frame.points = QVector<core::Point3f>{
|
||||
{0.00f, 0.00f, 0.00f},
|
||||
{0.10f, 0.00f, 0.00f},
|
||||
{0.20f, 0.00f, 0.00f},
|
||||
{0.00f, 0.10f, 0.00f},
|
||||
{0.10f, 0.10f, 0.02f},
|
||||
{0.20f, 0.10f, 0.00f},
|
||||
{0.00f, 0.20f, 0.00f},
|
||||
{0.10f, 0.20f, 0.00f},
|
||||
{0.20f, 0.20f, 0.01f}
|
||||
};
|
||||
frame.sourceLabel = "smoke-plugin-chain";
|
||||
const core::PipelineResult result = executor.run(frame);
|
||||
if (result.frame.points.isEmpty()) {
|
||||
failureReason = "Plugin chain produced empty frame.";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool runReconstructionPluginSwitchCase(QString &failureReason)
|
||||
{
|
||||
core::PipelineConfig config = core::makeDesktopDebugConfig();
|
||||
config.reconstructionPlugin = "pcl_greedy_triangulation";
|
||||
core::PipelineExecutor executor = factories::pipeline::createDesktopPipelineExecutor(config);
|
||||
|
||||
core::PointCloudFrame frame;
|
||||
frame.points = buildDemoCloud();
|
||||
frame.sourceLabel = "smoke-plugin-reconstruction";
|
||||
const core::PipelineResult result = executor.run(frame);
|
||||
if (result.triangles.isEmpty()) {
|
||||
failureReason = "Greedy plugin fallback produced zero triangles.";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace tests
|
||||
{
|
||||
bool runPipelineSmokeTests(QString &failureReason)
|
||||
{
|
||||
if (!runDemoCloudCase(failureReason)) {
|
||||
return false;
|
||||
}
|
||||
if (!runValidCloudCase(failureReason)) {
|
||||
return false;
|
||||
}
|
||||
if (!runNeighborRadiusCase(failureReason)) {
|
||||
return false;
|
||||
}
|
||||
if (!runRos2MockSourceCase(failureReason)) {
|
||||
return false;
|
||||
}
|
||||
if (!runRpiProfileCase(failureReason)) {
|
||||
return false;
|
||||
}
|
||||
if (!runPluginChainCase(failureReason)) {
|
||||
return false;
|
||||
}
|
||||
if (!runReconstructionPluginSwitchCase(failureReason)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace tests
|
||||
@@ -0,0 +1,11 @@
|
||||
#ifndef PIPELINE_SMOKE_TESTS_H
|
||||
#define PIPELINE_SMOKE_TESTS_H
|
||||
|
||||
#include <QString>
|
||||
|
||||
namespace tests
|
||||
{
|
||||
bool runPipelineSmokeTests(QString &failureReason);
|
||||
} // namespace tests
|
||||
|
||||
#endif // PIPELINE_SMOKE_TESTS_H
|
||||
@@ -0,0 +1,21 @@
|
||||
#include <QCoreApplication>
|
||||
#include <QTextStream>
|
||||
|
||||
#include "pipeline_smoke_tests.h"
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QCoreApplication app(argc, argv);
|
||||
QTextStream out(stdout);
|
||||
QTextStream err(stderr);
|
||||
|
||||
QString failureReason;
|
||||
const bool ok = tests::runPipelineSmokeTests(failureReason);
|
||||
if (ok) {
|
||||
out << "Smoke tests passed.\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
err << "Smoke tests failed: " << failureReason << "\n";
|
||||
return 1;
|
||||
}
|
||||
+4
-4
@@ -21,7 +21,7 @@ GlView::GlView(QWidget *parent)
|
||||
{
|
||||
}
|
||||
|
||||
void GlView::setData(const QVector<Point3f> &points, const QVector<Triangle> &triangles)
|
||||
void GlView::setData(const QVector<core::Point3f> &points, const QVector<core::Triangle> &triangles)
|
||||
{
|
||||
m_points = points;
|
||||
m_triangles = triangles;
|
||||
@@ -29,7 +29,7 @@ void GlView::setData(const QVector<Point3f> &points, const QVector<Triangle> &tr
|
||||
if (!m_points.isEmpty()) {
|
||||
QVector3D bmin(m_points[0].x, m_points[0].y, m_points[0].z);
|
||||
QVector3D bmax = bmin;
|
||||
for (const Point3f &p : m_points) {
|
||||
for (const core::Point3f &p : m_points) {
|
||||
bmin.setX(qMin(bmin.x(), p.x));
|
||||
bmin.setY(qMin(bmin.y(), p.y));
|
||||
bmin.setZ(qMin(bmin.z(), p.z));
|
||||
@@ -94,7 +94,7 @@ void GlView::paintGL()
|
||||
|
||||
QVector<float> vertices;
|
||||
vertices.reserve(m_points.size() * 3);
|
||||
for (const Point3f &p : m_points) {
|
||||
for (const core::Point3f &p : m_points) {
|
||||
vertices.push_back(p.x - m_center.x());
|
||||
vertices.push_back(p.y - m_center.y());
|
||||
vertices.push_back(p.z - m_center.z());
|
||||
@@ -104,7 +104,7 @@ void GlView::paintGL()
|
||||
indices.reserve(m_triangles.size() * 3);
|
||||
QVector<GLuint> edgeIndices;
|
||||
edgeIndices.reserve(m_triangles.size() * 6);
|
||||
for (const Triangle &t : m_triangles) {
|
||||
for (const core::Triangle &t : m_triangles) {
|
||||
indices.push_back(static_cast<GLuint>(t.i0));
|
||||
indices.push_back(static_cast<GLuint>(t.i1));
|
||||
indices.push_back(static_cast<GLuint>(t.i2));
|
||||
|
||||
+4
-4
@@ -8,7 +8,7 @@
|
||||
#include <QVector3D>
|
||||
#include <QVector>
|
||||
|
||||
#include "../geometry/surface_reconstruction.h"
|
||||
#include "../core/point_cloud_types.h"
|
||||
|
||||
class GlView : public QOpenGLWidget, protected QOpenGLFunctions
|
||||
{
|
||||
@@ -16,7 +16,7 @@ class GlView : public QOpenGLWidget, protected QOpenGLFunctions
|
||||
|
||||
public:
|
||||
explicit GlView(QWidget *parent = nullptr);
|
||||
void setData(const QVector<Point3f> &points, const QVector<Triangle> &triangles);
|
||||
void setData(const QVector<core::Point3f> &points, const QVector<core::Triangle> &triangles);
|
||||
void setSurfaceVisible(bool visible);
|
||||
|
||||
protected:
|
||||
@@ -28,8 +28,8 @@ protected:
|
||||
void wheelEvent(QWheelEvent *event) override;
|
||||
|
||||
private:
|
||||
QVector<Point3f> m_points;
|
||||
QVector<Triangle> m_triangles;
|
||||
QVector<core::Point3f> m_points;
|
||||
QVector<core::Triangle> m_triangles;
|
||||
QOpenGLShaderProgram m_program;
|
||||
float m_yawDeg;
|
||||
float m_pitchDeg;
|
||||
|
||||
+815
-312
File diff suppressed because it is too large
Load Diff
+130
-4
@@ -2,27 +2,153 @@
|
||||
#define MAINWINDOW_H
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <QVariantList>
|
||||
#include <QVariantMap>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QVector>
|
||||
|
||||
#include "../geometry/surface_reconstruction.h"
|
||||
#include "../algorithms/reconstruction/surface_reconstruction.h"
|
||||
#include "../core/pipeline_config.h"
|
||||
#include "../core/pipeline_executor.h"
|
||||
|
||||
class GlView;
|
||||
class QQuickWidget;
|
||||
class QDockWidget;
|
||||
|
||||
class MainWindow : public QMainWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QStringList stageChain READ stageChain NOTIFY stageChainChanged)
|
||||
Q_PROPERTY(QStringList availableStages READ availableStages CONSTANT)
|
||||
Q_PROPERTY(QStringList availableReconstructions READ availableReconstructions CONSTANT)
|
||||
Q_PROPERTY(QString reconstructionMethod READ reconstructionMethod WRITE setReconstructionMethod NOTIFY reconstructionMethodChanged)
|
||||
Q_PROPERTY(QString pipelinePreview READ pipelinePreview NOTIFY pipelinePreviewChanged)
|
||||
Q_PROPERTY(QString statusText READ statusText NOTIFY statusTextChanged)
|
||||
Q_PROPERTY(bool surfaceVisible READ surfaceVisible WRITE setSurfaceVisible NOTIFY surfaceVisibleChanged)
|
||||
Q_PROPERTY(QVariantList stageCards READ stageCards NOTIFY stageCardsChanged)
|
||||
Q_PROPERTY(QVariantList stageLibrary READ stageLibrary CONSTANT)
|
||||
Q_PROPERTY(int selectedStageIndex READ selectedStageIndex WRITE setSelectedStageIndex NOTIFY selectedStageIndexChanged)
|
||||
Q_PROPERTY(QVariantMap selectedStage READ selectedStage NOTIFY selectedStageChanged)
|
||||
Q_PROPERTY(QVariantMap metrics READ metrics NOTIFY metricsChanged)
|
||||
Q_PROPERTY(QString recommendation READ recommendation NOTIFY recommendationChanged)
|
||||
Q_PROPERTY(QString chainHealth READ chainHealth NOTIFY chainHealthChanged)
|
||||
Q_PROPERTY(QStringList demoSurfaceTypes READ demoSurfaceTypes CONSTANT)
|
||||
Q_PROPERTY(QString demoSurfaceType READ demoSurfaceType WRITE setDemoSurfaceType NOTIFY demoSurfaceTypeChanged)
|
||||
Q_PROPERTY(QString uiMode READ uiMode WRITE setUiMode NOTIFY uiModeChanged)
|
||||
Q_PROPERTY(QString wizardGoal READ wizardGoal WRITE setWizardGoal NOTIFY wizardGoalChanged)
|
||||
Q_PROPERTY(QString wizardProfile READ wizardProfile WRITE setWizardProfile NOTIFY wizardProfileChanged)
|
||||
Q_PROPERTY(QVariantList snapshots READ snapshots NOTIFY snapshotsChanged)
|
||||
|
||||
public:
|
||||
explicit MainWindow(QWidget *parent = nullptr);
|
||||
QStringList stageChain() const;
|
||||
QStringList availableStages() const;
|
||||
QStringList availableReconstructions() const;
|
||||
QString reconstructionMethod() const;
|
||||
QString pipelinePreview() const;
|
||||
QString statusText() const;
|
||||
bool surfaceVisible() const;
|
||||
QVariantList stageCards() const;
|
||||
QVariantList stageLibrary() const;
|
||||
int selectedStageIndex() const;
|
||||
QVariantMap selectedStage() const;
|
||||
QVariantMap metrics() const;
|
||||
QString recommendation() const;
|
||||
QString chainHealth() const;
|
||||
QStringList demoSurfaceTypes() const;
|
||||
QString demoSurfaceType() const;
|
||||
QString uiMode() const;
|
||||
QString wizardGoal() const;
|
||||
QString wizardProfile() const;
|
||||
QVariantList snapshots() const;
|
||||
|
||||
public slots:
|
||||
void generateDemo();
|
||||
void openPointsFile();
|
||||
void applyPreset(const QString &presetId);
|
||||
void setStageChain(const QStringList &chain);
|
||||
void addStage(const QString &stageId);
|
||||
void insertStage(int index, const QString &stageId);
|
||||
void removeStage(int index);
|
||||
void moveStage(int from, int to);
|
||||
void applyPipelineFromUi();
|
||||
void setReconstructionMethod(const QString &pluginId);
|
||||
void setSurfaceVisible(bool visible);
|
||||
void setNeighborRadius(double value);
|
||||
void setStageEnabled(int index, bool enabled);
|
||||
void setStageDefaults(int index, const QString &defaultsText);
|
||||
QString defaultsForStage(const QString &stageId) const;
|
||||
void setSelectedStageIndex(int index);
|
||||
void setDemoSurfaceType(const QString &surfaceType);
|
||||
void setUiMode(const QString &mode);
|
||||
void setWizardGoal(const QString &goal);
|
||||
void setWizardProfile(const QString &profile);
|
||||
void applyWizardSuggestion();
|
||||
void saveSnapshot(const QString &slotName);
|
||||
void loadSnapshot(const QString &slotName);
|
||||
|
||||
private:
|
||||
void rebuildSurface(const QVector<Point3f> &points, const QString &sourceLabel);
|
||||
bool loadPointsFromFile(const QString &filePath, QVector<Point3f> &points, QString &errorText) const;
|
||||
struct StageCardData {
|
||||
QString id;
|
||||
QString title;
|
||||
QString category;
|
||||
QString hint;
|
||||
QString defaults;
|
||||
bool enabled;
|
||||
};
|
||||
|
||||
void rebuildSurface(const QVector<core::Point3f> &points, const QString &sourceLabel);
|
||||
bool loadPointsFromFile(const QString &filePath, QVector<core::Point3f> &points, QString &errorText) const;
|
||||
void rebuildExecutorFromConfig();
|
||||
bool normalizeAndValidateConfig(core::PipelineConfig &config, QString &errorText) const;
|
||||
QString pipelineSummary() const;
|
||||
void updateStatusText(const QString &text);
|
||||
void notifyPipelineStateChanged();
|
||||
QVariantMap makeStageLibraryEntry(const StageCardData &card) const;
|
||||
void rebuildCardsFromConfig();
|
||||
void recomputeInsights();
|
||||
QString displayNameForStage(const QString &stageId) const;
|
||||
StageCardData createCardForStage(const QString &stageId, bool enabled = true) const;
|
||||
QStringList activeStageIds() const;
|
||||
|
||||
GlView *m_glView;
|
||||
QVector<Point3f> m_currentPoints;
|
||||
QQuickWidget *m_dashboardView;
|
||||
QDockWidget *m_dashboardDock;
|
||||
core::PipelineExecutor m_pipelineExecutor;
|
||||
core::PipelineConfig m_pipelineConfig;
|
||||
QVector<core::Point3f> m_currentPoints;
|
||||
QString m_currentSourceLabel;
|
||||
QString m_statusText;
|
||||
bool m_surfaceVisible;
|
||||
QVector<StageCardData> m_stageCards;
|
||||
int m_selectedStageIndex;
|
||||
QVariantMap m_metrics;
|
||||
QString m_recommendation;
|
||||
QString m_chainHealth;
|
||||
QString m_demoSurfaceType;
|
||||
QString m_uiMode;
|
||||
QString m_wizardGoal;
|
||||
QString m_wizardProfile;
|
||||
QVariantMap m_savedSnapshots;
|
||||
|
||||
signals:
|
||||
void stageChainChanged();
|
||||
void reconstructionMethodChanged();
|
||||
void pipelinePreviewChanged();
|
||||
void statusTextChanged();
|
||||
void surfaceVisibleChanged();
|
||||
void stageCardsChanged();
|
||||
void selectedStageIndexChanged();
|
||||
void selectedStageChanged();
|
||||
void metricsChanged();
|
||||
void recommendationChanged();
|
||||
void chainHealthChanged();
|
||||
void demoSurfaceTypeChanged();
|
||||
void uiModeChanged();
|
||||
void wizardGoalChanged();
|
||||
void wizardProfileChanged();
|
||||
void snapshotsChanged();
|
||||
};
|
||||
|
||||
#endif // MAINWINDOW_H
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
|
||||
GroupBox {
|
||||
title: "Stage Inspector"
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 4
|
||||
|
||||
Label {
|
||||
text: backend.selectedStage.title ? backend.selectedStage.title : "No stage selected"
|
||||
color: "#eaf0ff"
|
||||
font.bold: true
|
||||
font.pixelSize: 13
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
Label {
|
||||
text: backend.selectedStage.id ? ("id: " + backend.selectedStage.id) : ""
|
||||
color: "#9eb0d9"
|
||||
font.pixelSize: 10
|
||||
}
|
||||
Label {
|
||||
text: backend.selectedStage.hint ? backend.selectedStage.hint : "Select a stage to see usage hints."
|
||||
color: "#d0ddff"
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
font.pixelSize: 11
|
||||
}
|
||||
Label {
|
||||
text: backend.selectedStage.defaults ? ("Defaults: " + backend.selectedStage.defaults) : ""
|
||||
color: "#8fb4ff"
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
font.pixelSize: 11
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
|
||||
GroupBox {
|
||||
title: "Quick Wizard"
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 8
|
||||
|
||||
Label { text: "Data profile"; color: "#dbe7ff" }
|
||||
ComboBox {
|
||||
Layout.fillWidth: true
|
||||
model: ["general", "urban_scan", "indoor_object"]
|
||||
currentIndex: Math.max(0, find(backend.wizardProfile))
|
||||
onActivated: backend.setWizardProfile(currentText)
|
||||
}
|
||||
|
||||
Label { text: "Optimization goal"; color: "#dbe7ff" }
|
||||
ComboBox {
|
||||
Layout.fillWidth: true
|
||||
model: ["speed", "balanced", "quality"]
|
||||
currentIndex: Math.max(0, find(backend.wizardGoal))
|
||||
onActivated: backend.setWizardGoal(currentText)
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
color: "#9eb0d9"
|
||||
text: "Wizard builds a start chain and explains trade-offs."
|
||||
}
|
||||
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
text: "Generate suggested chain"
|
||||
onClicked: backend.applyWizardSuggestion()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../../shared/Theme.js" as Theme
|
||||
|
||||
GroupBox {
|
||||
title: "Сводка"
|
||||
readonly property var safeBackend: backend ? backend : ({
|
||||
chainHealth: "",
|
||||
recommendation: "",
|
||||
metrics: ({
|
||||
triangles: 0,
|
||||
reconstructMs: 0,
|
||||
inputPoints: 0,
|
||||
afterPreprocess: 0,
|
||||
removedPoints: 0,
|
||||
clusters: 0
|
||||
})
|
||||
})
|
||||
function localizedHealth(value) {
|
||||
if (value === "OK")
|
||||
return "ОК"
|
||||
if (value === "Warning")
|
||||
return "Предупреждение"
|
||||
if (value === "Risk")
|
||||
return "Риск"
|
||||
return value
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacingXs
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Label { text: "Состояние: " + localizedHealth(safeBackend.chainHealth); color: Theme.summaryPrimaryText; font.bold: true; font.pixelSize: Theme.fontMd }
|
||||
Item { Layout.fillWidth: true }
|
||||
Label { text: "Треугольники: " + safeBackend.metrics.triangles; color: Theme.summarySecondaryText; font.pixelSize: Theme.fontSm }
|
||||
Label { text: "Реконструкция, мс: " + safeBackend.metrics.reconstructMs; color: Theme.summarySecondaryText; font.pixelSize: Theme.fontSm }
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
text: safeBackend.recommendation
|
||||
color: Theme.summaryRecommendation
|
||||
font.pixelSize: Theme.fontSm
|
||||
}
|
||||
|
||||
Flow {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Theme.spacingXs / 2
|
||||
width: parent.width
|
||||
spacing: Theme.spacingLg
|
||||
clip: true
|
||||
Label { text: "Вход: " + safeBackend.metrics.inputPoints; color: Theme.summaryDetailText; font.pixelSize: Theme.fontXs }
|
||||
Label { text: "После препроцесса: " + safeBackend.metrics.afterPreprocess; color: Theme.summaryDetailText; font.pixelSize: Theme.fontXs }
|
||||
Label { text: "Удалено: " + safeBackend.metrics.removedPoints; color: Theme.summaryDetailText; font.pixelSize: Theme.fontXs }
|
||||
Label { text: "Кластеры: " + safeBackend.metrics.clusters; color: Theme.summaryDetailText; font.pixelSize: Theme.fontXs }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../../dialogs"
|
||||
import "../../shared/Theme.js" as Theme
|
||||
|
||||
GroupBox {
|
||||
id: root
|
||||
title: ""
|
||||
readonly property var safeBackend: backend ? backend : ({
|
||||
selectedStageIndex: -1,
|
||||
stageCards: [],
|
||||
reconstructionMethod: "surface_fallback",
|
||||
addStage: function(_) {},
|
||||
setReconstructionMethod: function(_) {},
|
||||
moveStage: function(_, __) {},
|
||||
setSelectedStageIndex: function(_) {},
|
||||
removeStage: function(_) {},
|
||||
setStageDefaults: function(_, __) {},
|
||||
defaultsForStage: function(_) { return ""; }
|
||||
})
|
||||
property var popupItem: ({})
|
||||
|
||||
property int selectedIndex: safeBackend.selectedStageIndex
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacingSm
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
radius: Theme.radiusMd
|
||||
color: Theme.cardBg
|
||||
border.width: 1
|
||||
border.color: Theme.cardBorder
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingSm
|
||||
spacing: Theme.spacingMd
|
||||
|
||||
Item {
|
||||
id: chainArea
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
|
||||
StageChainView {
|
||||
anchors.fill: parent
|
||||
safeBackend: root.safeBackend
|
||||
selectedIndex: root.selectedIndex
|
||||
onSettingsRequested: {
|
||||
root.popupItem = stageItem
|
||||
stageSettingsDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
id: paletteArea
|
||||
Layout.preferredWidth: Theme.palettePanelWidth
|
||||
Layout.fillHeight: true
|
||||
clip: false
|
||||
|
||||
StagePalette {
|
||||
anchors.fill: parent
|
||||
safeBackend: root.safeBackend
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StageSettingsDialog {
|
||||
id: stageSettingsDialog
|
||||
parent: root
|
||||
safeBackend: root.safeBackend
|
||||
popupItem: root.popupItem
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../../shared/PipelineUiCatalog.js" as UiCatalog
|
||||
import "../../shared/Theme.js" as Theme
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property var safeBackend
|
||||
property int selectedIndex: -1
|
||||
signal settingsRequested(var stageItem)
|
||||
|
||||
ListView {
|
||||
id: stageList
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: reconstructionCard.top
|
||||
anchors.bottomMargin: Theme.spacingSm
|
||||
clip: true
|
||||
model: root.safeBackend.stageCards
|
||||
|
||||
delegate: Rectangle {
|
||||
id: rowRoot
|
||||
property int itemIndex: index
|
||||
width: stageList.width
|
||||
height: Theme.stageRowHeight
|
||||
radius: Theme.radiusMd
|
||||
border.width: 1
|
||||
border.color: index === root.selectedIndex ? Theme.chainSelectedBorder : Theme.chainIdleBorder
|
||||
color: modelData.enabled ? Theme.chainEnabledBg : Theme.chainDisabledBg
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingSm
|
||||
spacing: Theme.spacingSm
|
||||
|
||||
Label {
|
||||
text: "↕"
|
||||
color: Theme.chainDragHandle
|
||||
font.pixelSize: Theme.fontLg
|
||||
width: Theme.stageHandleWidth
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
MouseArea {
|
||||
id: dragHandle
|
||||
anchors.fill: parent
|
||||
drag.target: dragProxy
|
||||
onPressed: root.safeBackend.setSelectedStageIndex(index)
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Label { text: modelData.title; color: Theme.paletteFilterText; font.bold: true; font.pixelSize: Theme.fontMd }
|
||||
Label { text: modelData.category + " | " + modelData.id; color: Theme.chainDragHandle; font.pixelSize: Theme.fontXs }
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: dragProxy
|
||||
visible: dragHandle.drag.active
|
||||
width: rowRoot.width
|
||||
height: rowRoot.height
|
||||
color: Theme.chainDragOverlay
|
||||
opacity: 0.35
|
||||
radius: Theme.radiusMd
|
||||
z: 3
|
||||
Drag.active: dragHandle.drag.active
|
||||
Drag.source: rowRoot
|
||||
Drag.hotSpot.x: width / 2
|
||||
Drag.hotSpot.y: height / 2
|
||||
}
|
||||
|
||||
DropArea {
|
||||
anchors.fill: parent
|
||||
onEntered: {
|
||||
if (drag.source && drag.source !== rowRoot && drag.source.itemIndex !== undefined) {
|
||||
root.safeBackend.moveStage(drag.source.itemIndex, index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: root.safeBackend.setSelectedStageIndex(index)
|
||||
onDoubleClicked: {
|
||||
root.safeBackend.setSelectedStageIndex(index)
|
||||
root.settingsRequested(modelData)
|
||||
}
|
||||
onPressed: {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
root.safeBackend.setSelectedStageIndex(index)
|
||||
stageMenu.popup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Menu {
|
||||
id: stageMenu
|
||||
MenuItem {
|
||||
text: "Удалить элемент"
|
||||
onTriggered: root.safeBackend.removeStage(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: reconstructionCard
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: Theme.stageRowHeight
|
||||
radius: Theme.radiusMd
|
||||
border.width: 1
|
||||
border.color: Theme.reconstructionBorder
|
||||
color: Theme.reconstructionBg
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingSm
|
||||
spacing: Theme.spacingSm
|
||||
|
||||
Label {
|
||||
text: "REC"
|
||||
color: Theme.reconstructionAccent
|
||||
font.bold: true
|
||||
font.pixelSize: Theme.fontSm
|
||||
width: Theme.reconstructionBadgeWidth
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Label {
|
||||
text: UiCatalog.reconstructionTitle(root.safeBackend.reconstructionMethod)
|
||||
color: Theme.reconstructionText
|
||||
font.bold: true
|
||||
font.pixelSize: Theme.fontMd
|
||||
}
|
||||
Label {
|
||||
text: "Реконструкция | " + root.safeBackend.reconstructionMethod
|
||||
color: Theme.reconstructionSubtext
|
||||
font.pixelSize: Theme.fontXs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../../shared/PipelineUiCatalog.js" as UiCatalog
|
||||
import "../../shared/Theme.js" as Theme
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property var safeBackend
|
||||
|
||||
Flickable {
|
||||
anchors.fill: parent
|
||||
contentWidth: width
|
||||
contentHeight: paletteColumn.implicitHeight + Theme.spacingMd
|
||||
clip: true
|
||||
|
||||
Column {
|
||||
id: paletteColumn
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXs
|
||||
|
||||
Label { text: "Фильтры"; font.bold: true; color: Theme.paletteHeader; padding: Theme.spacingXs }
|
||||
Repeater {
|
||||
model: UiCatalog.filterPaletteModel()
|
||||
delegate: Rectangle {
|
||||
property string paletteStageId: modelData.idValue
|
||||
width: paletteColumn.width
|
||||
height: Theme.paletteItemHeight
|
||||
radius: Theme.radiusSm
|
||||
color: Theme.paletteFilterBg
|
||||
border.width: 1
|
||||
border.color: Theme.paletteFilterBorder
|
||||
Label {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingMd
|
||||
text: modelData.title
|
||||
color: Theme.paletteFilterText
|
||||
font.pixelSize: Theme.fontSm
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onDoubleClicked: root.safeBackend.addStage(parent.paletteStageId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label { text: "Реконструкторы поверхности"; font.bold: true; color: Theme.paletteHeader; padding: Theme.spacingXs }
|
||||
Repeater {
|
||||
model: UiCatalog.reconstructionPaletteModel()
|
||||
delegate: Rectangle {
|
||||
property string paletteStageId: modelData.idValue
|
||||
width: paletteColumn.width
|
||||
height: Theme.paletteItemHeight
|
||||
radius: Theme.radiusSm
|
||||
color: Theme.paletteReconBg
|
||||
border.width: 1
|
||||
border.color: Theme.paletteReconBorder
|
||||
Label {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingMd
|
||||
text: modelData.title
|
||||
color: Theme.paletteReconText
|
||||
font.pixelSize: Theme.fontSm
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onDoubleClicked: root.safeBackend.setReconstructionMethod(parent.paletteStageId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../shared/PipelineUiCatalog.js" as UiCatalog
|
||||
import "../shared/Theme.js" as Theme
|
||||
|
||||
Dialog {
|
||||
id: root
|
||||
property var safeBackend
|
||||
property var popupItem: ({})
|
||||
property var paramItems: []
|
||||
|
||||
function updateParamValue(idx, newValue) {
|
||||
var next = paramItems.slice(0)
|
||||
var item = next[idx]
|
||||
item.value = String(newValue)
|
||||
next[idx] = item
|
||||
paramItems = next
|
||||
}
|
||||
|
||||
modal: true
|
||||
x: (parent.width - width) / 2
|
||||
y: (parent.height - height) / 2
|
||||
width: Theme.dialogWidth
|
||||
title: ""
|
||||
standardButtons: Dialog.NoButton
|
||||
|
||||
onAccepted: {
|
||||
if (safeBackend.selectedStageIndex >= 0) {
|
||||
var serialized = UiCatalog.serializeParams(root.paramItems)
|
||||
safeBackend.setStageDefaults(safeBackend.selectedStageIndex, serialized)
|
||||
popupItem.defaults = serialized
|
||||
}
|
||||
}
|
||||
|
||||
onOpened: root.paramItems = UiCatalog.parseDefaults(popupItem.defaults)
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: Theme.spacingSm
|
||||
Label {
|
||||
text: popupItem.title ? popupItem.title : "Элемент не выбран"
|
||||
font.bold: true
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Label {
|
||||
text: popupItem.hint ? popupItem.hint : "Для этого элемента нет подробного описания."
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Label {
|
||||
text: "Параметры"
|
||||
font.bold: true
|
||||
color: Theme.dialogSectionText
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Repeater {
|
||||
model: root.paramItems
|
||||
delegate: ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacingMd
|
||||
Label {
|
||||
text: modelData.key + ":"
|
||||
color: Theme.dialogHintText
|
||||
Layout.preferredWidth: Theme.dialogLabelWidth
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
ComboBox {
|
||||
Layout.fillWidth: true
|
||||
visible: modelData.kind === "bool"
|
||||
model: ["false", "true"]
|
||||
currentIndex: modelData.value === "true" ? 1 : 0
|
||||
onCurrentTextChanged: {
|
||||
if (modelData.kind === "bool")
|
||||
root.updateParamValue(index, currentText)
|
||||
}
|
||||
}
|
||||
TextField {
|
||||
Layout.fillWidth: true
|
||||
visible: modelData.kind === "int"
|
||||
text: modelData.value
|
||||
validator: IntValidator {}
|
||||
onEditingFinished: root.updateParamValue(index, text)
|
||||
}
|
||||
TextField {
|
||||
Layout.fillWidth: true
|
||||
visible: modelData.kind === "float"
|
||||
text: modelData.value
|
||||
validator: DoubleValidator {}
|
||||
onEditingFinished: root.updateParamValue(index, text)
|
||||
}
|
||||
TextField {
|
||||
Layout.fillWidth: true
|
||||
visible: modelData.kind === "string"
|
||||
text: modelData.value
|
||||
onEditingFinished: root.updateParamValue(index, text)
|
||||
}
|
||||
}
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: UiCatalog.paramDescription(modelData.key)
|
||||
color: Theme.dialogDescriptionText
|
||||
font.pixelSize: Theme.fontXs
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Theme.spacingMd
|
||||
spacing: Theme.spacingMd
|
||||
Button {
|
||||
text: "Установить по умолчанию"
|
||||
Layout.preferredWidth: Theme.dialogResetButtonWidth
|
||||
onClicked: root.paramItems = UiCatalog.parseDefaults(safeBackend.defaultsForStage(popupItem.id))
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
Button {
|
||||
text: "OK"
|
||||
Layout.preferredWidth: Theme.dialogOkButtonWidth
|
||||
onClicked: root.accept()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import QtQuick 2.12
|
||||
import QtQuick.Controls 2.12
|
||||
import QtQuick.Layouts 1.12
|
||||
import "../components/pipeline"
|
||||
import "../shared/Theme.js" as Theme
|
||||
|
||||
Rectangle {
|
||||
color: Theme.panelBg
|
||||
property int compactH: Theme.compactControlHeight
|
||||
readonly property var safeBackend: backend ? backend : ({
|
||||
surfaceVisible: true,
|
||||
demoSurfaceTypes: ["Сфера", "Тор", "Волна"],
|
||||
demoSurfaceType: "Сфера",
|
||||
openPointsFile: function() {},
|
||||
generateDemo: function() {},
|
||||
setDemoSurfaceType: function(_) {},
|
||||
setSurfaceVisible: function(_) {},
|
||||
applyPipelineFromUi: function() {}
|
||||
})
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingMd
|
||||
spacing: Theme.spacingSm
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacingSm
|
||||
ComboBox {
|
||||
Layout.preferredWidth: Theme.dashboardSurfaceComboWidth
|
||||
Layout.preferredHeight: compactH
|
||||
model: safeBackend.demoSurfaceTypes
|
||||
currentIndex: model.indexOf(safeBackend.demoSurfaceType)
|
||||
onActivated: safeBackend.setDemoSurfaceType(currentText)
|
||||
}
|
||||
Button { text: "Сгенерировать"; height: compactH; onClicked: safeBackend.generateDemo() }
|
||||
Button { text: "Загрузить"; height: compactH; onClicked: safeBackend.openPointsFile() }
|
||||
Item { Layout.fillWidth: true }
|
||||
}
|
||||
|
||||
PipelineCanvas {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacingSm
|
||||
Switch { checked: safeBackend.surfaceVisible; onToggled: safeBackend.setSurfaceVisible(checked) }
|
||||
Item { Layout.fillWidth: true }
|
||||
Button { text: "Применить"; Layout.preferredHeight: compactH; onClicked: safeBackend.applyPipelineFromUi() }
|
||||
}
|
||||
|
||||
MetricsStrip {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Theme.dashboardMetricsHeight
|
||||
Layout.bottomMargin: Theme.dashboardBottomMargin
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
.pragma library
|
||||
|
||||
function inferParamType(valueText) {
|
||||
var v = (valueText === undefined || valueText === null) ? "" : String(valueText).trim()
|
||||
if (v === "true" || v === "false")
|
||||
return "bool"
|
||||
if (/^-?\d+$/.test(v))
|
||||
return "int"
|
||||
if (/^-?(?:\d+\.\d*|\d*\.\d+)$/.test(v))
|
||||
return "float"
|
||||
return "string"
|
||||
}
|
||||
|
||||
function parseDefaults(defaultsText) {
|
||||
var out = []
|
||||
if (!defaultsText)
|
||||
return out
|
||||
var chunks = String(defaultsText).split(",")
|
||||
for (var i = 0; i < chunks.length; ++i) {
|
||||
var chunk = chunks[i].trim()
|
||||
if (!chunk)
|
||||
continue
|
||||
var eq = chunk.indexOf("=")
|
||||
var key = eq >= 0 ? chunk.slice(0, eq).trim() : chunk
|
||||
var value = eq >= 0 ? chunk.slice(eq + 1).trim() : ""
|
||||
out.push({ key: key, value: value, kind: inferParamType(value) })
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function serializeParams(items) {
|
||||
var parts = []
|
||||
for (var i = 0; i < items.length; ++i) {
|
||||
var p = items[i]
|
||||
parts.push(p.key + "=" + p.value)
|
||||
}
|
||||
return parts.join(",")
|
||||
}
|
||||
|
||||
function paramDescription(key) {
|
||||
if (key === "minCluster")
|
||||
return "Минимальный размер кластера в точках."
|
||||
if (key === "targetPoints")
|
||||
return "Целевое количество точек после прореживания."
|
||||
if (key === "leaf")
|
||||
return "Размер вокселя для Voxel Grid."
|
||||
if (key === "meanK")
|
||||
return "Число соседей для статистической оценки."
|
||||
if (key === "stddev")
|
||||
return "Порог отклонения для удаления выбросов."
|
||||
if (key === "radius")
|
||||
return "Радиус поиска соседей."
|
||||
if (key === "minNeighbors")
|
||||
return "Минимум соседей, чтобы точка считалась валидной."
|
||||
return "Параметр этапа обработки."
|
||||
}
|
||||
|
||||
function reconstructionTitle(id) {
|
||||
if (id === "surface_fallback")
|
||||
return "Fallback Surface"
|
||||
if (id === "pcl_greedy_triangulation")
|
||||
return "PCL Greedy Triangulation"
|
||||
return id
|
||||
}
|
||||
|
||||
function filterPaletteModel() {
|
||||
return [
|
||||
{ title: "Крупнейший кластер", idValue: "keep_largest_cluster" },
|
||||
{ title: "Прореживание плотности", idValue: "downsample_dense" },
|
||||
{ title: "PCL Voxel Grid", idValue: "pcl_voxel_grid" },
|
||||
{ title: "Статистическая фильтрация", idValue: "pcl_statistical_outlier" },
|
||||
{ title: "Радиусная фильтрация", idValue: "pcl_radius_outlier" }
|
||||
]
|
||||
}
|
||||
|
||||
function reconstructionPaletteModel() {
|
||||
return [
|
||||
{ title: "Fallback Surface", idValue: "surface_fallback" },
|
||||
{ title: "PCL Greedy Triangulation", idValue: "pcl_greedy_triangulation" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
.pragma library
|
||||
|
||||
var panelBg = "#141821"
|
||||
var cardBg = "#111a2c"
|
||||
var cardBorder = "#33486f"
|
||||
|
||||
var chainSelectedBorder = "#6ea8ff"
|
||||
var chainIdleBorder = "#384866"
|
||||
var chainEnabledBg = "#1f2a3d"
|
||||
var chainDisabledBg = "#23272f"
|
||||
var chainDragOverlay = "#39507d"
|
||||
var chainDragHandle = "#9fb4da"
|
||||
|
||||
var reconstructionBg = "#2d2438"
|
||||
var reconstructionBorder = "#625481"
|
||||
var reconstructionAccent = "#d9c8ff"
|
||||
var reconstructionText = "#efe7ff"
|
||||
var reconstructionSubtext = "#c3b2e8"
|
||||
|
||||
var paletteFilterBg = "#26344f"
|
||||
var paletteFilterBorder = "#3d547d"
|
||||
var paletteReconBg = "#3a2f44"
|
||||
var paletteReconBorder = "#625481"
|
||||
var paletteHeader = "#d6e3ff"
|
||||
var paletteFilterText = "#eaf0ff"
|
||||
var paletteReconText = "#efe7ff"
|
||||
|
||||
var summaryPrimaryText = "#eaf0ff"
|
||||
var summarySecondaryText = "#cde0ff"
|
||||
var summaryRecommendation = "#9ec3ff"
|
||||
var summaryDetailText = "#b8cff8"
|
||||
|
||||
var dialogHintText = "#4a4f63"
|
||||
var dialogDescriptionText = "#7e869d"
|
||||
var dialogSectionText = "#9eb0d9"
|
||||
|
||||
var spacingXs = 4
|
||||
var spacingSm = 6
|
||||
var spacingMd = 8
|
||||
var spacingLg = 10
|
||||
var radiusSm = 6
|
||||
var radiusMd = 8
|
||||
var compactControlHeight = 28
|
||||
|
||||
var dashboardSurfaceComboWidth = 170
|
||||
var dashboardMetricsHeight = 96
|
||||
var dashboardBottomMargin = 8
|
||||
var palettePanelWidth = 260
|
||||
|
||||
var stageRowHeight = 48
|
||||
var stageHandleWidth = 16
|
||||
var reconstructionBadgeWidth = 28
|
||||
var paletteItemHeight = 30
|
||||
|
||||
var dialogWidth = 430
|
||||
var dialogLabelWidth = 150
|
||||
var dialogResetButtonWidth = 190
|
||||
var dialogOkButtonWidth = 90
|
||||
|
||||
var fontXs = 10
|
||||
var fontSm = 11
|
||||
var fontMd = 12
|
||||
var fontLg = 14
|
||||
@@ -0,0 +1,14 @@
|
||||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file>pages/PipelineDashboard.qml</file>
|
||||
<file>components/WizardPanel.qml</file>
|
||||
<file>components/StageInspector.qml</file>
|
||||
<file>components/pipeline/PipelineCanvas.qml</file>
|
||||
<file>components/pipeline/StageChainView.qml</file>
|
||||
<file>components/pipeline/StagePalette.qml</file>
|
||||
<file>components/pipeline/MetricsStrip.qml</file>
|
||||
<file>dialogs/StageSettingsDialog.qml</file>
|
||||
<file>shared/PipelineUiCatalog.js</file>
|
||||
<file>shared/Theme.js</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
Reference in New Issue
Block a user