Добавил конструктор фильтров

This commit is contained in:
2026-04-22 17:10:25 +03:00
parent c8e553e953
commit e0f72ba288
60 changed files with 4041 additions and 379 deletions
+79 -5
View File
@@ -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
+82
View File
@@ -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
+31 -2
View File
@@ -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
+17
View File
@@ -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
+21
View File
@@ -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
+19
View File
@@ -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
+23
View File
@@ -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
@@ -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
@@ -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
+16
View File
@@ -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
+38
View File
@@ -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
+69
View File
@@ -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
+113
View File
@@ -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
+35
View File
@@ -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
+52
View File
@@ -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
+40
View File
@@ -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
+83
View File
@@ -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
+78
View File
@@ -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
+30
View File
@@ -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
+53
View File
@@ -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
+52
View File
@@ -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
+30
View File
@@ -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
+27
View File
@@ -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
+28
View File
@@ -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
+28
View File
@@ -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
+196
View File
@@ -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
+11
View File
@@ -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
+21
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+130 -4
View File
@@ -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
+39
View File
@@ -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
}
}
}
+41
View File
@@ -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)
}
}
}
}
}
}
+128
View File
@@ -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()
}
}
}
}
+61
View File
@@ -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
}
}
}
+81
View File
@@ -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" }
]
}
+63
View File
@@ -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
+14
View File
@@ -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>