Initial commit with correct .gitignore
This commit is contained in:
+47
@@ -0,0 +1,47 @@
|
||||
# Build directories
|
||||
build/
|
||||
debug/
|
||||
release/
|
||||
|
||||
# Qt generated files
|
||||
Makefile*
|
||||
*.pro.user
|
||||
*.pro.user.*
|
||||
*.qmake.stash
|
||||
*.qmake.cache
|
||||
moc_*.cpp
|
||||
ui_*.h
|
||||
qrc_*.cpp
|
||||
|
||||
# Object and library files
|
||||
*.o
|
||||
*.obj
|
||||
*.so
|
||||
*.a
|
||||
*.lib
|
||||
*.dll
|
||||
*.dylib
|
||||
|
||||
# Executables
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
|
||||
# Qt deployment folders
|
||||
platforms/
|
||||
styles/
|
||||
imageformats/
|
||||
iconengines/
|
||||
translations/
|
||||
bearer/
|
||||
|
||||
# Common logs/temp files
|
||||
*.log
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
# OS/editor files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.swp
|
||||
*.swo
|
||||
@@ -0,0 +1,17 @@
|
||||
QT += core gui widgets opengl
|
||||
|
||||
CONFIG += c++11
|
||||
TEMPLATE = app
|
||||
TARGET = DotsToSirface
|
||||
LIBS += -lopengl32
|
||||
|
||||
SOURCES += \
|
||||
src/main.cpp \
|
||||
src/geometry/surface_reconstruction.cpp \
|
||||
src/ui/mainwindow.cpp \
|
||||
src/ui/glview.cpp
|
||||
|
||||
HEADERS += \
|
||||
src/geometry/surface_reconstruction.h \
|
||||
src/ui/mainwindow.h \
|
||||
src/ui/glview.h
|
||||
@@ -0,0 +1,48 @@
|
||||
# DotsToSirface (Qt 5.11)
|
||||
|
||||
Демонстрационная программа на Qt 5.11 C++, которая:
|
||||
- принимает массив 3D-точек `QVector<Point3f>`;
|
||||
- строит массив треугольников `QVector<Triangle>`;
|
||||
- визуализирует точки и полученную треугольную поверхность.
|
||||
|
||||
## Вход/выход API
|
||||
|
||||
`src/geometry/surface_reconstruction.h`:
|
||||
|
||||
- `struct Point3f { float x, y, z; };`
|
||||
- `struct Triangle { int i0, i1, i2; };`
|
||||
- `QVector<Triangle> buildSurfaceTriangles(const QVector<Point3f>& points);`
|
||||
|
||||
`Triangle` хранит индексы вершин в исходном массиве `points`.
|
||||
|
||||
## Как это работает
|
||||
|
||||
Реализация использует инкрементальный `Convex Hull 3D`:
|
||||
- удаление дубликатов точек (epsilon-сравнение);
|
||||
- поиск стартового тетраэдра;
|
||||
- поочередное добавление точек с пересчетом видимых граней и горизонта;
|
||||
- поддержание ориентированных наружу треугольников.
|
||||
|
||||
## Ограничения
|
||||
|
||||
Текущая реализация строит **выпуклую оболочку** облака точек.
|
||||
Для невыпуклых объектов и детальной реконструкции произвольной поверхности нужны более сложные алгоритмы (например, alpha-shapes, Poisson reconstruction и т.п.).
|
||||
|
||||
## Сборка
|
||||
|
||||
Пример для Qt 5.11:
|
||||
|
||||
```bash
|
||||
qmake DotsToSirface.pro
|
||||
make
|
||||
```
|
||||
|
||||
Для Windows/MSVC используйте соответствующий `nmake`/`jom`.
|
||||
|
||||
## Демо
|
||||
|
||||
При запуске приложение:
|
||||
- генерирует тестовое облако точек (приближенная сфера с небольшим шумом);
|
||||
- строит триангуляцию;
|
||||
- показывает статистику: количество точек, количество треугольников и время построения;
|
||||
- отображает сцену в `QOpenGLWidget` (ЛКМ - вращение, колесо - зум).
|
||||
@@ -0,0 +1,186 @@
|
||||
#include "surface_reconstruction.h"
|
||||
|
||||
#include <QSet>
|
||||
#include <QString>
|
||||
#include <QtGlobal>
|
||||
#include <QtMath>
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
|
||||
namespace
|
||||
{
|
||||
const double kEps = 1e-6;
|
||||
float gNeighborRadiusScale = 3.5f;
|
||||
|
||||
struct Point2d
|
||||
{
|
||||
double x;
|
||||
double y;
|
||||
};
|
||||
|
||||
struct UniquePoint
|
||||
{
|
||||
Point3f p;
|
||||
int originalIndex;
|
||||
};
|
||||
|
||||
QString xyKey(const Point3f &p)
|
||||
{
|
||||
const qint64 qx = qRound64(static_cast<double>(p.x) / kEps);
|
||||
const qint64 qy = qRound64(static_cast<double>(p.y) / kEps);
|
||||
return QString::number(qx) + "_" + QString::number(qy);
|
||||
}
|
||||
|
||||
QVector<UniquePoint> uniquePointsByXY(const QVector<Point3f> &input)
|
||||
{
|
||||
QVector<UniquePoint> out;
|
||||
out.reserve(input.size());
|
||||
QSet<QString> seen;
|
||||
|
||||
for (int i = 0; i < input.size(); ++i) {
|
||||
const QString key = xyKey(input[i]);
|
||||
if (!seen.contains(key)) {
|
||||
seen.insert(key);
|
||||
out.push_back({input[i], i});
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
double orient2d(const Point2d &a, const Point2d &b, const Point2d &c)
|
||||
{
|
||||
return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
|
||||
}
|
||||
|
||||
struct Neighbor
|
||||
{
|
||||
int idx;
|
||||
double d2;
|
||||
double angle;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void setNeighborRadiusScale(const float scale)
|
||||
{
|
||||
gNeighborRadiusScale = qBound(0.5f, scale, 20.0f);
|
||||
}
|
||||
|
||||
float neighborRadiusScale()
|
||||
{
|
||||
return gNeighborRadiusScale;
|
||||
}
|
||||
|
||||
QVector<Triangle> buildSurfaceTriangles(const QVector<Point3f> &points)
|
||||
{
|
||||
QVector<Triangle> triangles;
|
||||
QVector<UniquePoint> pts = uniquePointsByXY(points);
|
||||
if (pts.size() < 4) {
|
||||
return triangles;
|
||||
}
|
||||
|
||||
QVector<Point2d> pts2d;
|
||||
pts2d.reserve(pts.size());
|
||||
for (const UniquePoint &up : pts) {
|
||||
pts2d.push_back({up.p.x, up.p.y});
|
||||
}
|
||||
|
||||
auto dist2xyz = [&](const int a, const int b) -> double {
|
||||
const double dx = static_cast<double>(pts[a].p.x) - static_cast<double>(pts[b].p.x);
|
||||
const double dy = static_cast<double>(pts[a].p.y) - static_cast<double>(pts[b].p.y);
|
||||
const double dz = static_cast<double>(pts[a].p.z) - static_cast<double>(pts[b].p.z);
|
||||
return dx * dx + dy * dy + dz * dz;
|
||||
};
|
||||
|
||||
QVector<double> nearestDist;
|
||||
nearestDist.reserve(pts.size());
|
||||
for (int i = 0; i < pts.size(); ++i) {
|
||||
double best = std::numeric_limits<double>::max();
|
||||
for (int j = 0; j < pts.size(); ++j) {
|
||||
if (i == j) {
|
||||
continue;
|
||||
}
|
||||
const double d2 = dist2xyz(i, j);
|
||||
if (d2 < best) {
|
||||
best = d2;
|
||||
}
|
||||
}
|
||||
if (best < std::numeric_limits<double>::max()) {
|
||||
nearestDist.push_back(qSqrt(best));
|
||||
}
|
||||
}
|
||||
if (nearestDist.isEmpty()) {
|
||||
return triangles;
|
||||
}
|
||||
std::sort(nearestDist.begin(), nearestDist.end());
|
||||
const double medianNearest = nearestDist[nearestDist.size() / 2];
|
||||
const double maxNeighborDistance = qMax(1e-6, medianNearest * static_cast<double>(gNeighborRadiusScale));
|
||||
const double maxNeighborDistance2 = maxNeighborDistance * maxNeighborDistance;
|
||||
const int kNeighbors = 10;
|
||||
|
||||
QSet<QString> uniqueTriangles;
|
||||
triangles.reserve(pts.size() * 2);
|
||||
|
||||
auto triangleKey = [](int a, int b, int c) -> QString {
|
||||
int v[3] = {a, b, c};
|
||||
std::sort(v, v + 3);
|
||||
return QString::number(v[0]) + "_" + QString::number(v[1]) + "_" + QString::number(v[2]);
|
||||
};
|
||||
|
||||
for (int i = 0; i < pts.size(); ++i) {
|
||||
QVector<Neighbor> neighbors;
|
||||
neighbors.reserve(pts.size() - 1);
|
||||
for (int j = 0; j < pts.size(); ++j) {
|
||||
if (i == j) {
|
||||
continue;
|
||||
}
|
||||
const double d2 = dist2xyz(i, j);
|
||||
if (d2 <= maxNeighborDistance2) {
|
||||
const double angle = qAtan2(pts2d[j].y - pts2d[i].y, pts2d[j].x - pts2d[i].x);
|
||||
neighbors.push_back({j, d2, angle});
|
||||
}
|
||||
}
|
||||
if (neighbors.size() < 3) {
|
||||
continue;
|
||||
}
|
||||
std::sort(neighbors.begin(), neighbors.end(), [](const Neighbor &a, const Neighbor &b) {
|
||||
return a.d2 < b.d2;
|
||||
});
|
||||
if (neighbors.size() > kNeighbors) {
|
||||
neighbors.resize(kNeighbors);
|
||||
}
|
||||
std::sort(neighbors.begin(), neighbors.end(), [](const Neighbor &a, const Neighbor &b) {
|
||||
return a.angle < b.angle;
|
||||
});
|
||||
|
||||
for (int n = 0; n < neighbors.size(); ++n) {
|
||||
const int j = neighbors[n].idx;
|
||||
const int k = neighbors[(n + 1) % neighbors.size()].idx;
|
||||
if (j == k) {
|
||||
continue;
|
||||
}
|
||||
if (dist2xyz(j, k) > maxNeighborDistance2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const double area2 = orient2d(pts2d[i], pts2d[j], pts2d[k]);
|
||||
if (qAbs(area2) < kEps) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QString key = triangleKey(i, j, k);
|
||||
if (uniqueTriangles.contains(key)) {
|
||||
continue;
|
||||
}
|
||||
uniqueTriangles.insert(key);
|
||||
|
||||
if (area2 > 0.0) {
|
||||
triangles.push_back({pts[i].originalIndex, pts[j].originalIndex, pts[k].originalIndex});
|
||||
} else {
|
||||
triangles.push_back({pts[i].originalIndex, pts[k].originalIndex, pts[j].originalIndex});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return triangles;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#ifndef SURFACE_RECONSTRUCTION_H
|
||||
#define SURFACE_RECONSTRUCTION_H
|
||||
|
||||
#include <QVector>
|
||||
|
||||
struct Point3f
|
||||
{
|
||||
float x;
|
||||
float y;
|
||||
float z;
|
||||
};
|
||||
|
||||
struct Triangle
|
||||
{
|
||||
int i0;
|
||||
int i1;
|
||||
int i2;
|
||||
};
|
||||
|
||||
void setNeighborRadiusScale(float scale);
|
||||
float neighborRadiusScale();
|
||||
QVector<Triangle> buildSurfaceTriangles(const QVector<Point3f> &points);
|
||||
|
||||
#endif // SURFACE_RECONSTRUCTION_H
|
||||
@@ -0,0 +1,13 @@
|
||||
#include <QApplication>
|
||||
|
||||
#include "ui/mainwindow.h"
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QCoreApplication::setAttribute(Qt::AA_UseSoftwareOpenGL);
|
||||
QApplication app(argc, argv);
|
||||
MainWindow window;
|
||||
window.resize(1100, 800);
|
||||
window.show();
|
||||
return app.exec();
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
#include "glview.h"
|
||||
|
||||
#include <QMatrix4x4>
|
||||
#include <QMouseEvent>
|
||||
#include <QVector3D>
|
||||
#include <QWheelEvent>
|
||||
#include <QtMath>
|
||||
|
||||
GlView::GlView(QWidget *parent)
|
||||
: QOpenGLWidget(parent)
|
||||
, m_yawDeg(-30.0f)
|
||||
, m_pitchDeg(25.0f)
|
||||
, m_distance(4.0f)
|
||||
, m_minDistance(0.5f)
|
||||
, m_maxDistance(2000.0f)
|
||||
, m_center(0.0f, 0.0f, 0.0f)
|
||||
, m_radius(1.0f)
|
||||
, m_panX(0.0f)
|
||||
, m_panY(0.0f)
|
||||
, m_surfaceVisible(true)
|
||||
{
|
||||
}
|
||||
|
||||
void GlView::setData(const QVector<Point3f> &points, const QVector<Triangle> &triangles)
|
||||
{
|
||||
m_points = points;
|
||||
m_triangles = triangles;
|
||||
|
||||
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) {
|
||||
bmin.setX(qMin(bmin.x(), p.x));
|
||||
bmin.setY(qMin(bmin.y(), p.y));
|
||||
bmin.setZ(qMin(bmin.z(), p.z));
|
||||
bmax.setX(qMax(bmax.x(), p.x));
|
||||
bmax.setY(qMax(bmax.y(), p.y));
|
||||
bmax.setZ(qMax(bmax.z(), p.z));
|
||||
}
|
||||
|
||||
m_center = (bmin + bmax) * 0.5f;
|
||||
m_radius = qMax(1e-3f, (bmax - bmin).length() * 0.5f);
|
||||
m_distance = qMax(2.5f * m_radius, 1.0f);
|
||||
m_minDistance = qMax(0.05f * m_radius, 0.01f);
|
||||
m_maxDistance = qMax(100.0f * m_radius, m_distance * 2.0f);
|
||||
m_panX = 0.0f;
|
||||
m_panY = 0.0f;
|
||||
}
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void GlView::setSurfaceVisible(const bool visible)
|
||||
{
|
||||
if (m_surfaceVisible == visible) {
|
||||
return;
|
||||
}
|
||||
m_surfaceVisible = visible;
|
||||
update();
|
||||
}
|
||||
|
||||
void GlView::initializeGL()
|
||||
{
|
||||
initializeOpenGLFunctions();
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glDisable(GL_CULL_FACE);
|
||||
|
||||
m_program.addShaderFromSourceCode(QOpenGLShader::Vertex,
|
||||
"attribute vec3 aPos;\n"
|
||||
"uniform mat4 uMvp;\n"
|
||||
"void main() {\n"
|
||||
" gl_Position = uMvp * vec4(aPos, 1.0);\n"
|
||||
"}\n");
|
||||
m_program.addShaderFromSourceCode(QOpenGLShader::Fragment,
|
||||
"uniform vec3 uColor;\n"
|
||||
"void main() {\n"
|
||||
" gl_FragColor = vec4(uColor, 1.0);\n"
|
||||
"}\n");
|
||||
m_program.link();
|
||||
}
|
||||
|
||||
void GlView::resizeGL(int w, int h)
|
||||
{
|
||||
glViewport(0, 0, w, h);
|
||||
}
|
||||
|
||||
void GlView::paintGL()
|
||||
{
|
||||
glClearColor(0.08f, 0.09f, 0.11f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
if (m_points.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QVector<float> vertices;
|
||||
vertices.reserve(m_points.size() * 3);
|
||||
for (const 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());
|
||||
}
|
||||
|
||||
QVector<GLuint> indices;
|
||||
indices.reserve(m_triangles.size() * 3);
|
||||
QVector<GLuint> edgeIndices;
|
||||
edgeIndices.reserve(m_triangles.size() * 6);
|
||||
for (const 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));
|
||||
|
||||
edgeIndices.push_back(static_cast<GLuint>(t.i0));
|
||||
edgeIndices.push_back(static_cast<GLuint>(t.i1));
|
||||
edgeIndices.push_back(static_cast<GLuint>(t.i1));
|
||||
edgeIndices.push_back(static_cast<GLuint>(t.i2));
|
||||
edgeIndices.push_back(static_cast<GLuint>(t.i2));
|
||||
edgeIndices.push_back(static_cast<GLuint>(t.i0));
|
||||
}
|
||||
|
||||
QMatrix4x4 projection;
|
||||
projection.perspective(45.0f,
|
||||
static_cast<float>(width()) / qMax(1, height()),
|
||||
qMax(0.001f, m_distance * 0.001f),
|
||||
qMax(m_distance + 4.0f * m_radius, m_distance * 2.0f));
|
||||
|
||||
QMatrix4x4 view;
|
||||
view.translate(m_panX, m_panY, -m_distance);
|
||||
view.rotate(m_pitchDeg, 1.0f, 0.0f, 0.0f);
|
||||
view.rotate(m_yawDeg, 0.0f, 1.0f, 0.0f);
|
||||
|
||||
const QMatrix4x4 mvp = projection * view;
|
||||
|
||||
m_program.bind();
|
||||
m_program.setUniformValue("uMvp", mvp);
|
||||
m_program.enableAttributeArray("aPos");
|
||||
m_program.setAttributeArray("aPos", GL_FLOAT, vertices.constData(), 3);
|
||||
|
||||
if (m_surfaceVisible) {
|
||||
glEnable(GL_POLYGON_OFFSET_FILL);
|
||||
glPolygonOffset(1.0f, 1.0f);
|
||||
m_program.setUniformValue("uColor", QVector3D(0.20f, 0.72f, 0.95f));
|
||||
if (!indices.isEmpty()) {
|
||||
glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, indices.constData());
|
||||
}
|
||||
glDisable(GL_POLYGON_OFFSET_FILL);
|
||||
|
||||
glLineWidth(2.2f);
|
||||
m_program.setUniformValue("uColor", QVector3D(1.0f, 0.2f, 0.08f));
|
||||
if (!edgeIndices.isEmpty()) {
|
||||
glDrawElements(GL_LINES, edgeIndices.size(), GL_UNSIGNED_INT, edgeIndices.constData());
|
||||
}
|
||||
}
|
||||
|
||||
glPointSize(4.0f);
|
||||
m_program.setUniformValue("uColor", QVector3D(1.0f, 0.85f, 0.2f));
|
||||
glDrawArrays(GL_POINTS, 0, m_points.size());
|
||||
|
||||
m_program.disableAttributeArray("aPos");
|
||||
m_program.release();
|
||||
}
|
||||
|
||||
void GlView::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
m_lastMousePos = event->pos();
|
||||
}
|
||||
|
||||
void GlView::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
const QPoint delta = event->pos() - m_lastMousePos;
|
||||
m_lastMousePos = event->pos();
|
||||
|
||||
if (event->buttons() & Qt::LeftButton) {
|
||||
m_yawDeg += static_cast<float>(delta.x()) * 0.5f;
|
||||
m_pitchDeg += static_cast<float>(delta.y()) * 0.5f;
|
||||
m_pitchDeg = qBound(-89.0f, m_pitchDeg, 89.0f);
|
||||
update();
|
||||
} else if (event->buttons() & (Qt::RightButton | Qt::MiddleButton)) {
|
||||
const float panScale = qMax(0.0005f * m_distance, 0.0005f);
|
||||
m_panX += static_cast<float>(delta.x()) * panScale;
|
||||
m_panY -= static_cast<float>(delta.y()) * panScale;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void GlView::wheelEvent(QWheelEvent *event)
|
||||
{
|
||||
const float step = static_cast<float>(event->angleDelta().y()) / 120.0f;
|
||||
const float zoomFactor = qPow(1.15f, step);
|
||||
m_distance /= zoomFactor;
|
||||
m_distance = qBound(m_minDistance, m_distance, m_maxDistance);
|
||||
update();
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
#ifndef GLVIEW_H
|
||||
#define GLVIEW_H
|
||||
|
||||
#include <QOpenGLFunctions>
|
||||
#include <QOpenGLShaderProgram>
|
||||
#include <QOpenGLWidget>
|
||||
#include <QPoint>
|
||||
#include <QVector3D>
|
||||
#include <QVector>
|
||||
|
||||
#include "../geometry/surface_reconstruction.h"
|
||||
|
||||
class GlView : public QOpenGLWidget, protected QOpenGLFunctions
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit GlView(QWidget *parent = nullptr);
|
||||
void setData(const QVector<Point3f> &points, const QVector<Triangle> &triangles);
|
||||
void setSurfaceVisible(bool visible);
|
||||
|
||||
protected:
|
||||
void initializeGL() override;
|
||||
void resizeGL(int w, int h) override;
|
||||
void paintGL() override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
void wheelEvent(QWheelEvent *event) override;
|
||||
|
||||
private:
|
||||
QVector<Point3f> m_points;
|
||||
QVector<Triangle> m_triangles;
|
||||
QOpenGLShaderProgram m_program;
|
||||
float m_yawDeg;
|
||||
float m_pitchDeg;
|
||||
float m_distance;
|
||||
float m_minDistance;
|
||||
float m_maxDistance;
|
||||
QVector3D m_center;
|
||||
float m_radius;
|
||||
float m_panX;
|
||||
float m_panY;
|
||||
bool m_surfaceVisible;
|
||||
QPoint m_lastMousePos;
|
||||
};
|
||||
|
||||
#endif // GLVIEW_H
|
||||
@@ -0,0 +1,416 @@
|
||||
#include "mainwindow.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QElapsedTimer>
|
||||
#include <QFile>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QHash>
|
||||
#include <QInputDialog>
|
||||
#include <QMessageBox>
|
||||
#include <QMenuBar>
|
||||
#include <QRegularExpression>
|
||||
#include <QRandomGenerator>
|
||||
#include <QStringList>
|
||||
#include <QStatusBar>
|
||||
#include <QTextStream>
|
||||
#include <QtMath>
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
|
||||
#include "../geometry/surface_reconstruction.h"
|
||||
#include "glview.h"
|
||||
|
||||
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];
|
||||
}
|
||||
};
|
||||
|
||||
QVector<Point3f> generateDemoPoints(const int count)
|
||||
{
|
||||
QVector<Point3f> points;
|
||||
points.reserve(count);
|
||||
QRandomGenerator rng(42u);
|
||||
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const float u = rng.generateDouble() * 2.0f - 1.0f;
|
||||
const float theta = rng.generateDouble() * 2.0f * static_cast<float>(M_PI);
|
||||
const float r = 1.0f + (rng.generateDouble() * 0.08f - 0.04f);
|
||||
const float s = qSqrt(qMax(0.0f, 1.0f - u * u));
|
||||
|
||||
Point3f p;
|
||||
p.x = r * s * qCos(theta);
|
||||
p.y = r * s * qSin(theta);
|
||||
p.z = r * u;
|
||||
points.push_back(p);
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
QVector<Point3f> keepLargestCluster(const QVector<Point3f> &points, int &removedCount, int &clusterCount)
|
||||
{
|
||||
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];
|
||||
// Conservative threshold: keeps local neighborhood connected,
|
||||
// while detached sparse groups are separated.
|
||||
const double joinDistance = qMax(1e-9, medianNn * 5.0);
|
||||
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<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<Point3f> downsampleDenseAreas(const QVector<Point3f> &points, int &removedCount)
|
||||
{
|
||||
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 * 0.8);
|
||||
|
||||
struct Bucket
|
||||
{
|
||||
double sx;
|
||||
double sy;
|
||||
double sz;
|
||||
int n;
|
||||
};
|
||||
QHash<QString, Bucket> buckets;
|
||||
buckets.reserve(points.size());
|
||||
|
||||
for (const 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<Point3f> out;
|
||||
out.reserve(buckets.size());
|
||||
for (auto it = buckets.constBegin(); it != buckets.constEnd(); ++it) {
|
||||
const Bucket &b = it.value();
|
||||
out.push_back({
|
||||
static_cast<float>(b.sx / b.n),
|
||||
static_cast<float>(b.sy / b.n),
|
||||
static_cast<float>(b.sz / b.n)
|
||||
});
|
||||
}
|
||||
|
||||
removedCount = points.size() - out.size();
|
||||
return out;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
MainWindow::MainWindow(QWidget *parent)
|
||||
: QMainWindow(parent)
|
||||
, m_glView(new GlView(this))
|
||||
{
|
||||
setWindowTitle("DotsToSirface - Surface Triangulation Demo");
|
||||
setCentralWidget(m_glView);
|
||||
|
||||
QAction *loadAction = new QAction("Load points (.txt/.csv)...", this);
|
||||
QAction *generateDemoAction = new QAction("Generate demo points", this);
|
||||
QAction *showSurfaceAction = new QAction("Show surface", this);
|
||||
QAction *neighborRadiusAction = new QAction("Neighbor radius...", this);
|
||||
showSurfaceAction->setCheckable(true);
|
||||
showSurfaceAction->setChecked(true);
|
||||
menuBar()->addAction(loadAction);
|
||||
menuBar()->addAction(generateDemoAction);
|
||||
menuBar()->addAction(showSurfaceAction);
|
||||
menuBar()->addAction(neighborRadiusAction);
|
||||
|
||||
connect(generateDemoAction, &QAction::triggered, this, [this]() {
|
||||
const QVector<Point3f> points = generateDemoPoints(350);
|
||||
rebuildSurface(points, "built-in demo");
|
||||
});
|
||||
|
||||
connect(loadAction, &QAction::triggered, this, [this]() {
|
||||
const QString filePath = QFileDialog::getOpenFileName(
|
||||
this,
|
||||
"Open points file",
|
||||
QString(),
|
||||
"Point files (*.txt *.csv *.bin);;Binary files (*.bin);;Text files (*.txt);;CSV files (*.csv);;All files (*.*)");
|
||||
if (filePath.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QVector<Point3f> points;
|
||||
QString errorText;
|
||||
if (!loadPointsFromFile(filePath, points, errorText)) {
|
||||
QMessageBox::warning(this, "Load error", errorText);
|
||||
return;
|
||||
}
|
||||
|
||||
int removedCount = 0;
|
||||
int clusterCount = 1;
|
||||
const QVector<Point3f> clustered = keepLargestCluster(points, removedCount, clusterCount);
|
||||
|
||||
int denseRemoved = 0;
|
||||
const QVector<Point3f> filteredPoints = downsampleDenseAreas(clustered, denseRemoved);
|
||||
|
||||
const int totalRemoved = removedCount + denseRemoved;
|
||||
const QString source = totalRemoved > 0
|
||||
? QString("%1 (clusters: %2, removed: %3, dense: %4)")
|
||||
.arg(QFileInfo(filePath).fileName())
|
||||
.arg(clusterCount)
|
||||
.arg(totalRemoved)
|
||||
.arg(denseRemoved)
|
||||
: QFileInfo(filePath).fileName();
|
||||
rebuildSurface(filteredPoints, source);
|
||||
});
|
||||
|
||||
connect(showSurfaceAction, &QAction::toggled, this, [this](const bool enabled) {
|
||||
m_glView->setSurfaceVisible(enabled);
|
||||
});
|
||||
|
||||
connect(neighborRadiusAction, &QAction::triggered, this, [this]() {
|
||||
bool ok = false;
|
||||
const double value = QInputDialog::getDouble(
|
||||
this,
|
||||
"Neighbor radius",
|
||||
"Radius scale (0.5 .. 20.0):",
|
||||
static_cast<double>(neighborRadiusScale()),
|
||||
0.5,
|
||||
20.0,
|
||||
2,
|
||||
&ok);
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
setNeighborRadiusScale(static_cast<float>(value));
|
||||
if (!m_currentPoints.isEmpty()) {
|
||||
rebuildSurface(m_currentPoints, m_currentSourceLabel);
|
||||
} else {
|
||||
statusBar()->showMessage(QString("Neighbor radius scale set to %1").arg(value), 3500);
|
||||
}
|
||||
});
|
||||
|
||||
const QVector<Point3f> points = generateDemoPoints(350);
|
||||
rebuildSurface(points, "built-in demo");
|
||||
}
|
||||
|
||||
void MainWindow::rebuildSurface(const QVector<Point3f> &points, const QString &sourceLabel)
|
||||
{
|
||||
m_currentPoints = points;
|
||||
m_currentSourceLabel = sourceLabel;
|
||||
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
const QVector<Triangle> triangles = buildSurfaceTriangles(points);
|
||||
const qint64 elapsedMs = timer.elapsed();
|
||||
|
||||
m_glView->setData(points, triangles);
|
||||
|
||||
statusBar()->showMessage(
|
||||
QString("Source: %1 | Points: %2 | Triangles: %3 | Time: %4 ms")
|
||||
.arg(sourceLabel)
|
||||
.arg(points.size())
|
||||
.arg(triangles.size())
|
||||
.arg(elapsedMs));
|
||||
}
|
||||
|
||||
bool MainWindow::loadPointsFromFile(const QString &filePath, QVector<Point3f> &points, QString &errorText) const
|
||||
{
|
||||
const QString ext = QFileInfo(filePath).suffix().toLower();
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
errorText = QString("Cannot open file: %1").arg(filePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
QTextStream stream(&file);
|
||||
points.clear();
|
||||
if (ext == "bin") {
|
||||
const QByteArray raw = file.readAll();
|
||||
if (raw.size() % static_cast<int>(sizeof(float) * 3) != 0) {
|
||||
errorText = "Invalid .bin size: expected multiples of 3 floats (x,y,z).";
|
||||
return false;
|
||||
}
|
||||
|
||||
const int pointCount = raw.size() / static_cast<int>(sizeof(float) * 3);
|
||||
points.reserve(pointCount);
|
||||
const float *values = reinterpret_cast<const float *>(raw.constData());
|
||||
for (int i = 0; i < pointCount; ++i) {
|
||||
const int k = i * 3;
|
||||
points.push_back({values[k], values[k + 1], values[k + 2]});
|
||||
}
|
||||
} else {
|
||||
stream.seek(0);
|
||||
int lineNo = 0;
|
||||
while (!stream.atEnd()) {
|
||||
QString line = stream.readLine();
|
||||
++lineNo;
|
||||
line = line.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)) {
|
||||
errorText = QString("Invalid numeric values at line %1").arg(lineNo);
|
||||
points.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
points.push_back({x, y, z});
|
||||
}
|
||||
}
|
||||
|
||||
if (points.size() < 4) {
|
||||
errorText = "Need at least 4 valid 3D points in file.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#ifndef MAINWINDOW_H
|
||||
#define MAINWINDOW_H
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
#include "../geometry/surface_reconstruction.h"
|
||||
|
||||
class GlView;
|
||||
|
||||
class MainWindow : public QMainWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MainWindow(QWidget *parent = nullptr);
|
||||
|
||||
private:
|
||||
void rebuildSurface(const QVector<Point3f> &points, const QString &sourceLabel);
|
||||
bool loadPointsFromFile(const QString &filePath, QVector<Point3f> &points, QString &errorText) const;
|
||||
|
||||
GlView *m_glView;
|
||||
QVector<Point3f> m_currentPoints;
|
||||
QString m_currentSourceLabel;
|
||||
};
|
||||
|
||||
#endif // MAINWINDOW_H
|
||||
Reference in New Issue
Block a user