From 517889ff90d982799a0452e5353549461c90ae00 Mon Sep 17 00:00:00 2001 From: YangOne Date: Mon, 13 Jul 2026 18:56:01 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E5=8F=96CloudView3D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CloudUtils/CloudUtils.pro | 18 +- CloudView/CloudView.pro | 138 +- CloudView/Inc/CloudViewMainWindow.h | 26 +- CloudView/Inc/PointCloudConverter.h | 61 +- CloudView/Inc/PointCloudGLWidget.h | 497 +- CloudView/Src/CloudViewMainWindow.cpp | 7045 +++++++++-------- CloudView3D/CloudView3D.pro | 30 + CloudView3D/Inc/CloudView3D.h | 7 + CloudView3D/Inc/CloudView3DTypes.h | 54 + CloudView3D/Inc/PointCloudGLWidget.h | 521 ++ .../Src/PointCloudGLWidget.cpp | 71 +- DataUtils/CloudMathClac/CloudMathClac.pro | 4 +- Utils.pro | 20 +- VrCommon/Inc/VZNL_Types.h | 4656 +++++------ 14 files changed, 6691 insertions(+), 6457 deletions(-) create mode 100644 CloudView3D/CloudView3D.pro create mode 100644 CloudView3D/Inc/CloudView3D.h create mode 100644 CloudView3D/Inc/CloudView3DTypes.h create mode 100644 CloudView3D/Inc/PointCloudGLWidget.h rename {CloudView => CloudView3D}/Src/PointCloudGLWidget.cpp (94%) diff --git a/CloudUtils/CloudUtils.pro b/CloudUtils/CloudUtils.pro index 508a650..d62a055 100644 --- a/CloudUtils/CloudUtils.pro +++ b/CloudUtils/CloudUtils.pro @@ -1,9 +1,9 @@ -# CloudUtils库项目配置文件 +# CloudUtils搴撻」鐩厤缃枃浠? -# 指定Qt模块 +# 鎸囧畾Qt妯″潡 QT += core -# 指定项目模板为静态库 +# 鎸囧畾椤圭洰妯℃澘涓洪潤鎬佸簱 TEMPLATE = lib CONFIG += staticlib @@ -12,24 +12,24 @@ win32-msvc { } -# 指定C++标准 +# 鎸囧畾C++鏍囧噯 CONFIG += c++11 -INCLUDEPATH += $$PWD/../../SDK/Device/VzNLSDK/_Inc -INCLUDEPATH += $$PWD/../../SDK/Device/VzNLSDK/Inc +INCLUDEPATH += $$PWD/../../Device/SDK/VzNLSDK/_Inc +INCLUDEPATH += $$PWD/../../Device/SDK/VzNLSDK/Inc INCLUDEPATH += $$PWD/../VrCommon/Inc INCLUDEPATH += $$PWD/../VrUtils/Inc -# 指定包含路径 +# 鎸囧畾鍖呭惈璺緞 INCLUDEPATH += ./Inc \ ./Src -# 指定头文件 +# 鎸囧畾澶存枃浠? HEADERS += Inc/LaserDataLoader.h \ Inc/PointCloudImageUtils.h -# 指定源文件 +# 鎸囧畾婧愭枃浠? SOURCES += Src/LaserDataLoader.cpp \ Src/PointCloudImageUtils.cpp diff --git a/CloudView/CloudView.pro b/CloudView/CloudView.pro index 946f617..adcf38a 100644 --- a/CloudView/CloudView.pro +++ b/CloudView/CloudView.pro @@ -1,68 +1,70 @@ -# Qt 5 兼容:QOpenGLWidget 在 widgets 模块中 -QT += core gui widgets opengl - -CONFIG += c++17 -CONFIG -= app_bundle - -# Windows平台UTF-8编码支持 -win32-msvc { - QMAKE_CXXFLAGS += /utf-8 - QMAKE_CXXFLAGS += /bigobj -} - -TARGET = CloudView -TEMPLATE = app - -# 项目目录 -INCLUDEPATH += $$PWD/Inc -INCLUDEPATH += $$PWD/../CloudUtils/Inc -INCLUDEPATH += $$PWD/../../SDK/Device/VzNLSDK/Inc -INCLUDEPATH += $$PWD/../VrUtils/Inc -INCLUDEPATH += $$PWD/../VrCommon/Inc - -# 头文件 -HEADERS += \ - Inc/CloudViewMainWindow.h \ - Inc/PointCloudGLWidget.h \ - Inc/PointCloudConverter.h - -# 源文件 -SOURCES += \ - main.cpp \ - Src/CloudViewMainWindow.cpp \ - Src/PointCloudGLWidget.cpp \ - Src/PointCloudConverter.cpp - -# 资源文件 -RESOURCES += resource/resource.qrc -RESOURCES += $$PWD/../../AppUtils/UICommon/common_resources.qrc - -# Windows 应用图标 -RC_ICONS = resource/logo.ico - -# Windows平台库链接 -win32:CONFIG(release, debug|release): { - LIBS += -L../CloudUtils/release -lCloudUtils - LIBS += -L../VrUtils/release -lVrUtils - LIBS += -L../VrCommon/release -lVrCommon -} -else:win32:CONFIG(debug, debug|release): { - LIBS += -L../CloudUtils/debug -lCloudUtils - LIBS += -L../VrUtils/debug -lVrUtils - LIBS += -L../VrCommon/debug -lVrCommon -} - -# Windows系统库 -win32 { - LIBS += -lAdvapi32 - LIBS += -lopengl32 - LIBS += -lglu32 -} - - -DEFINES += _USE_MATH_DEFINES - -# Default rules for deployment -qnx: target.path = /tmp/$${TARGET}/bin -else: unix:!android: target.path = /opt/$${TARGET}/bin -!isEmpty(target.path): INSTALLS += target +# Qt 5 鍏煎锛歈OpenGLWidget 鍦?widgets 妯″潡涓? +QT += core gui widgets opengl + +CONFIG += c++17 +CONFIG -= app_bundle + +# Windows骞冲彴UTF-8缂栫爜鏀寔 +win32-msvc { + QMAKE_CXXFLAGS += /utf-8 + QMAKE_CXXFLAGS += /bigobj +} + +TARGET = CloudView +TEMPLATE = app + +# 椤圭洰鐩綍 +INCLUDEPATH += $$PWD/Inc +INCLUDEPATH += $$PWD/../CloudView3D/Inc +INCLUDEPATH += $$PWD/../CloudUtils/Inc +INCLUDEPATH += $$PWD/../../Device/SDK/VzNLSDK/Inc +INCLUDEPATH += $$PWD/../VrUtils/Inc +INCLUDEPATH += $$PWD/../VrCommon/Inc + +# 澶存枃浠? +HEADERS += \ + Inc/CloudViewMainWindow.h \ + Inc/PointCloudGLWidget.h \ + Inc/PointCloudConverter.h + +# 婧愭枃浠? +SOURCES += \ + main.cpp \ + Src/CloudViewMainWindow.cpp \ + Src/PointCloudConverter.cpp + +# 璧勬簮鏂囦欢 +RESOURCES += resource/resource.qrc +RESOURCES += $$PWD/../../AppUtils/UICommon/common_resources.qrc + +# Windows 搴旂敤鍥炬爣 +RC_ICONS = resource/logo.ico + +# Windows骞冲彴搴撻摼鎺? +win32:CONFIG(release, debug|release): { + LIBS += -L../CloudView3D/release -lCloudView3D + LIBS += -L../CloudUtils/release -lCloudUtils + LIBS += -L../VrUtils/release -lVrUtils + LIBS += -L../VrCommon/release -lVrCommon +} +else:win32:CONFIG(debug, debug|release): { + LIBS += -L../CloudView3D/debug -lCloudView3D + LIBS += -L../CloudUtils/debug -lCloudUtils + LIBS += -L../VrUtils/debug -lVrUtils + LIBS += -L../VrCommon/debug -lVrCommon +} + +# Windows绯荤粺搴? +win32 { + LIBS += -lAdvapi32 + LIBS += -lopengl32 + LIBS += -lglu32 +} + + +DEFINES += _USE_MATH_DEFINES + +# Default rules for deployment +qnx: target.path = /tmp/$${TARGET}/bin +else: unix:!android: target.path = /opt/$${TARGET}/bin +!isEmpty(target.path): INSTALLS += target diff --git a/CloudView/Inc/CloudViewMainWindow.h b/CloudView/Inc/CloudViewMainWindow.h index e64216a..eabe549 100644 --- a/CloudView/Inc/CloudViewMainWindow.h +++ b/CloudView/Inc/CloudViewMainWindow.h @@ -22,8 +22,8 @@ #include #include -#include "PointCloudGLWidget.h" -#include "PointCloudConverter.h" +#include "CloudView3D.h" +#include "PointCloudConverter.h" class QTextEdit; class QTableWidget; @@ -276,9 +276,14 @@ private: /** * @brief 更新选点信息显示 */ - void updateSelectedPointsDisplay(); - - /** + void updateSelectedPointsDisplay(); + void clearMeasureResultDisplay(); + void updateMeasureResultDisplay(const SelectedPointInfo& p1, + const SelectedPointInfo& p2, + float distance, + const QString& statusPrefix = QString()); + + /** * @brief 从原始数据获取线上点(包含0,0,0) */ QVector getOriginalLinePoints(const SelectedLineInfo& lineInfo); @@ -307,7 +312,9 @@ private: bool loadBoundingBoxFile(const QString& fileName); void applyTransformToAllClouds(const QMatrix4x4& matrix); void addGeneratedCloud(const PointCloudXYZ& cloud, const QString& name); - void showBasicShape(const QString& shapeName, const QVector& segments); + void showBasicShape(const QString& shapeName, + const QVector& segments, + const QVector& points = QVector()); void showBasicSurface(const QString& shapeName, const QVector& surfaces, const QVector& outlines); // 点云显示控件 @@ -329,9 +336,10 @@ private: // 选点测距控件 QCheckBox* m_cbMeasureDistance; QPushButton* m_btnClearPoints; - QLabel* m_lblPoint1; - QLabel* m_lblPoint2; - QLabel* m_lblDistance; + QLabel* m_lblPoint1; + QLabel* m_lblPoint2; + QLabel* m_lblDistance; + QLabel* m_lblDeltaXYZ; // 点1坐标编辑控件 QLineEdit* m_editPoint1X; diff --git a/CloudView/Inc/PointCloudConverter.h b/CloudView/Inc/PointCloudConverter.h index 18a7ded..170c859 100644 --- a/CloudView/Inc/PointCloudConverter.h +++ b/CloudView/Inc/PointCloudConverter.h @@ -1,59 +1,14 @@ #ifndef POINT_CLOUD_CONVERTER_H #define POINT_CLOUD_CONVERTER_H -#include -#include -#include - -/** - * @brief 简单的 3D 点结构 - */ -struct Point3D -{ - float x, y, z; - Point3D() : x(0), y(0), z(0) {} - Point3D(float _x, float _y, float _z) : x(_x), y(_y), z(_z) {} -}; - -/** - * @brief 带颜色的 3D 点结构 - */ -struct Point3DRGB -{ - float x, y, z; - uint8_t r, g, b; - float pointSize; // 自定义点大小,0 表示使用全局默认值(来自 RGBA 中 A > 1 的值) - Point3DRGB() : x(0), y(0), z(0), r(255), g(255), b(255), pointSize(0) {} - Point3DRGB(float _x, float _y, float _z, uint8_t _r = 255, uint8_t _g = 255, uint8_t _b = 255, float _ps = 0) - : x(_x), y(_y), z(_z), r(_r), g(_g), b(_b), pointSize(_ps) {} -}; - -/** - * @brief 简单的点云容器 - */ -template -class SimplePointCloud -{ -public: - std::vector points; - std::vector lineIndices; // 每个点所属的线索引 - - void clear() { points.clear(); lineIndices.clear(); } - size_t size() const { return points.size(); } - bool empty() const { return points.empty(); } - void reserve(size_t n) { points.reserve(n); lineIndices.reserve(n); } - void push_back(const PointT& pt) { points.push_back(pt); } - void push_back(const PointT& pt, int lineIdx) { - points.push_back(pt); - lineIndices.push_back(lineIdx); - } -}; - -using PointCloudXYZ = SimplePointCloud; -using PointCloudXYZRGB = SimplePointCloud; - -/** - * @brief 点云数据转换器 +#include +#include +#include + +#include "CloudView3DTypes.h" + +/** + * @brief 点云数据转换器 * 负责加载 txt/dat、pcd、ply 格式的点云文件 */ class PointCloudConverter diff --git a/CloudView/Inc/PointCloudGLWidget.h b/CloudView/Inc/PointCloudGLWidget.h index 64fe9f6..d54df38 100644 --- a/CloudView/Inc/PointCloudGLWidget.h +++ b/CloudView/Inc/PointCloudGLWidget.h @@ -1,495 +1,6 @@ -#ifndef POINT_CLOUD_GL_WIDGET_H -#define POINT_CLOUD_GL_WIDGET_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "PointCloudConverter.h" -#include "VZNL_Types.h" - -/** - * @brief 点云显示颜色枚举 - */ -enum class PointCloudColor -{ - White, - Red, - Green, - Blue, - Yellow, - Cyan, - Magenta, - Original -}; - -/** - * @brief 选中的点信息 - */ -struct SelectedPointInfo -{ - bool valid; - size_t index; - float x, y, z; - int cloudIndex; - int lineIndex; // 所属线索引 - int pointIndexInLine; // 点在线中的索引 - - SelectedPointInfo() : valid(false), index(0), x(0), y(0), z(0), cloudIndex(-1), lineIndex(-1), pointIndexInLine(-1) {} -}; - -/** - * @brief 选线模式 - */ -enum class LineSelectMode -{ - Vertical, // 纵向选线(同一条扫描线) - Horizontal // 横向选线(所有线的相同索引点) -}; - -/** - * @brief 欧拉角旋转顺序 - */ -enum class EulerRotationOrder -{ - ZYX, // Yaw-Pitch-Roll(最常用) - XYZ, // Roll-Pitch-Yaw - ZXY, // Yaw-Roll-Pitch - YXZ, // Pitch-Roll-Yaw - XZY, // Roll-Yaw-Pitch - YZX // Pitch-Yaw-Roll -}; - -/** - * @brief 选中的线信息 - */ -struct SelectedLineInfo -{ - bool valid; - int cloudIndex; // 点云索引 - int lineIndex; // 线索引(纵向选线时使用) - int pointIndex; // 点索引(横向选线时使用) - int pointCount; // 该线上的点数 - LineSelectMode mode; // 选线模式 - - SelectedLineInfo() : valid(false), cloudIndex(-1), lineIndex(-1), pointIndex(-1), pointCount(0), mode(LineSelectMode::Vertical) {} -}; - -/** - * @brief 线段数据 - */ -struct LineSegment -{ - float x1, y1, z1; // 起点 - float x2, y2, z2; // 终点 - float r, g, b; // 颜色 (0-1) - float lineWidth; // 线宽,0 表示使用默认值(来自 RGBA 中 A > 1 的值) - - LineSegment() : x1(0), y1(0), z1(0), x2(0), y2(0), z2(0), r(1), g(1), b(1), lineWidth(0) {} - LineSegment(float _x1, float _y1, float _z1, float _x2, float _y2, float _z2, float _r = 1.0f, float _g = 1.0f, float _b = 1.0f, float _lw = 0) - : x1(_x1), y1(_y1), z1(_z1), x2(_x2), y2(_y2), z2(_z2), r(_r), g(_g), b(_b), lineWidth(_lw) {} -}; +#ifndef CLOUD_VIEW_POINT_CLOUD_GL_WIDGET_FORWARDER_H +#define CLOUD_VIEW_POINT_CLOUD_GL_WIDGET_FORWARDER_H -/** - * @brief 面片数据 - */ -struct SurfaceQuad -{ - QVector3D p1, p2, p3, p4; - float r, g, b, a; +#include "../../CloudView3D/Inc/PointCloudGLWidget.h" - SurfaceQuad() : p1(), p2(), p3(), p4(), r(0.0f), g(0.8f), b(1.0f), a(0.5f) {} - SurfaceQuad(const QVector3D& _p1, - const QVector3D& _p2, - const QVector3D& _p3, - const QVector3D& _p4, - float _r = 0.0f, - float _g = 0.8f, - float _b = 1.0f, - float _a = 0.5f) - : p1(_p1), p2(_p2), p3(_p3), p4(_p4), r(_r), g(_g), b(_b), a(_a) {} -}; - -/** - * @brief 姿态点数据 - * - * 坐标系约定(右手坐标系): - * - X轴:红色,指向右 - * - Y轴:绿色,指向上 - * - Z轴:蓝色,指向观察者 - * - * 欧拉角旋转顺序:ZYX(Yaw-Pitch-Roll) - * - rz: 绕Z轴旋转(偏航角,Yaw) - * - ry: 绕Y轴旋转(俯仰角,Pitch) - * - rx: 绕X轴旋转(滚转角,Roll) - */ -struct PosePoint -{ - float x, y, z; // 位置 - float rx, ry, rz; // 欧拉角(度) - float scale; // 坐标系大小 - - PosePoint() : x(0), y(0), z(0), rx(0), ry(0), rz(0), scale(10.0f) {} - PosePoint(float _x, float _y, float _z, float _rx, float _ry, float _rz, float _scale = 10.0f) - : x(_x), y(_y), z(_z), rx(_rx), ry(_ry), rz(_rz), scale(_scale) {} -}; - -/** - * @brief 点云 OpenGL 渲染控件 - */ -class PointCloudGLWidget : public QOpenGLWidget, protected QOpenGLFunctions -{ - Q_OBJECT - -public: - explicit PointCloudGLWidget(QWidget* parent = nullptr); - ~PointCloudGLWidget() override; - - void addPointCloud(const PointCloudXYZ& cloud, const QString& name = ""); - void addPointCloud(const PointCloudXYZRGB& cloud, const QString& name = ""); - void clearPointClouds(); - void setPointCloudColor(PointCloudColor color); - void setPointSize(float size); - void resetView(); - void fitToContent(); - void zoomIn(); - void zoomOut(); - /** - * @brief 设置视角旋转角度(不改变缩放和平移) - * @param rotX X轴旋转角度(俯仰) - * @param rotY Y轴旋转角度(偏航) - * @param rotZ Z轴旋转角度(滚转) - */ - void setViewAngles(float rotX, float rotY, float rotZ = 0.0f); - - QVector getSelectedPoints() const { return m_selectedPoints; } - SelectedLineInfo getSelectedLine() const { return m_selectedLine; } - void clearSelectedPoints(); - void clearSelectedLine(); - float calculateDistance(const SelectedPointInfo& p1, const SelectedPointInfo& p2); - - /** - * @brief 更新选中点的坐标 - * @param index 点索引(0或1) - * @param x 新的X坐标 - * @param y 新的Y坐标 - * @param z 新的Z坐标 - */ - void updateSelectedPointCoord(int index, float x, float y, float z); - - /** - * @brief 设置选中点(如果不存在则创建) - * @param index 点索引(0或1) - * @param x X坐标 - * @param y Y坐标 - * @param z Z坐标 - */ - void setSelectedPointCoord(int index, float x, float y, float z); - - /** - * @brief 通过线索引选择线(纵向) - */ - bool selectLineByIndex(int lineIndex); - - /** - * @brief 通过点索引选择横向线(所有线的相同索引点) - */ - bool selectHorizontalLineByIndex(int pointIndex); - - /** - * @brief 设置选线模式 - */ - void setLineSelectMode(LineSelectMode mode) { m_lineSelectMode = mode; } - - /** - * @brief 设置欧拉角旋转顺序 - */ - void setEulerRotationOrder(EulerRotationOrder order) { m_eulerRotationOrder = order; } - - /** - * @brief 获取欧拉角旋转顺序 - */ - EulerRotationOrder getEulerRotationOrder() const { return m_eulerRotationOrder; } - - /** - * @brief 设置是否启用测距功能 - */ - void setMeasureDistanceEnabled(bool enabled) { m_measureDistanceEnabled = enabled; } - - /** - * @brief 获取测距功能是否启用 - */ - bool isMeasureDistanceEnabled() const { return m_measureDistanceEnabled; } - - /** - * @brief 获取选中线上的所有点坐标 - * @return 点坐标列表,每个元素为 (x, y, z) - */ - QVector getSelectedLinePoints() const; - - /** - * @brief 设置列表高亮点(与选点功能区分) - * @param point 点坐标,如果为空则清除高亮 - */ - void setListHighlightPoint(const QVector3D& point); - - /** - * @brief 清除列表高亮点 - */ - void clearListHighlightPoint(); - - /** - * @brief 获取第一个点云的数据(用于旋转和保存) - */ - bool getFirstCloudData(PointCloudXYZ& cloud) const; - - /** - * @brief 替换第一个点云(用于显示旋转后的数据) - */ - void replaceFirstCloud(const PointCloudXYZ& cloud, const QString& name); - - /** - * @brief 获取点云数量 - */ - size_t getCloudCount() const { return m_pointClouds.size(); } - - /** - * @brief 对所有点云应用 4x4 变换矩阵 - * @param matrix 4x4 变换矩阵 - */ - void transformAllClouds(const QMatrix4x4& matrix); - - /** - * @brief 对所有点云中 Z 值小于阈值的点应用旋转矩阵 - * @param rotMatrix 旋转矩阵 - * @param zThreshold Z 轴阈值 - * @return 被旋转的点数 - */ - size_t rotateCloudsByZThreshold(const QMatrix4x4& rotMatrix, float zThreshold); - - /** - * @brief 获取所有点云数据(按线分组),用于保存 - * @param scanLines 输出:每条扫描线的点列表 - * @return 总点数 - */ - size_t getAllCloudsByLines(std::vector>& scanLines) const; - - /** - * @brief 添加线段 - */ - void addLineSegments(const QVector& segments); - - /** - * @brief 清除所有线段 - */ - void clearLineSegments(); - - /** - * @brief 设置基础图形线段 - */ - void setBasicShapeSegments(const QVector& segments); - - /** - * @brief 追加基础图形线段 - */ - void appendBasicShapeSegments(const QVector& segments); - - /** - * @brief 追加基础图形面片 - */ - void appendBasicShapeSurfaces(const QVector& surfaces); - - /** - * @brief 清除基础图形线段 - */ - void clearBasicShapeSegments(); - - /** - * @brief 添加姿态点 - */ - void addPosePoints(const QVector& poses); - - /** - * @brief 清除所有姿态点 - */ - void clearPosePoints(); - -signals: - void pointSelected(const SelectedPointInfo& point); - void twoPointsSelected(const SelectedPointInfo& p1, const SelectedPointInfo& p2, float distance); - void lineSelected(const SelectedLineInfo& line); - void viewAnglesChanged(float rotX, float rotY, float rotZ); - -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 mouseReleaseEvent(QMouseEvent* event) override; - void wheelEvent(QWheelEvent* event) override; - void mouseDoubleClickEvent(QMouseEvent* event) override; - void keyPressEvent(QKeyEvent* event) override; - -private: - struct PointCloudData - { - std::vector vertices; - std::vector colors; - std::vector lineIndices; // 每个点所属的线索引 - std::vector originalIndices; // 每个显示点在原始点云中的索引(用于计算原始index) - std::vector pointInLineIndices; // 每个显示点在所属线中的索引(预计算,支持不等长线) - bool hasColor; - bool hasLineInfo; // 是否有线信息 - QString name; - int colorIndex; // 点云颜色索引 - int totalLines; // 总线数 - int pointsPerLine; // 每线点数(网格化点云) - - // 自定义点大小分组(RGBA 中 A > 1 的点按大小分组) - struct PointSizeGroup { - float pointSize; // 点大小 - std::vector indices; // 该大小的点在顶点数组中的索引(顶点索引 = indices[i] * 3) - }; - std::vector customPointSizeGroups; - bool hasCustomPointSizes; - - // VBO 缓冲区 - QOpenGLBuffer vertexBuffer; // 顶点 VBO - QOpenGLBuffer colorBuffer; // 颜色 VBO - bool vboCreated; // VBO 是否已创建 - - PointCloudData() - : hasColor(false), hasLineInfo(false), colorIndex(0) - , totalLines(0), pointsPerLine(0) - , hasCustomPointSizes(false) - , vertexBuffer(QOpenGLBuffer::VertexBuffer) - , colorBuffer(QOpenGLBuffer::VertexBuffer) - , vboCreated(false) - {} - - // 移动构造函数(QOpenGLBuffer 不支持拷贝) - PointCloudData(PointCloudData&& other) noexcept - : vertices(std::move(other.vertices)) - , colors(std::move(other.colors)) - , lineIndices(std::move(other.lineIndices)) - , originalIndices(std::move(other.originalIndices)) - , pointInLineIndices(std::move(other.pointInLineIndices)) - , hasColor(other.hasColor) - , hasLineInfo(other.hasLineInfo) - , name(std::move(other.name)) - , colorIndex(other.colorIndex) - , totalLines(other.totalLines) - , pointsPerLine(other.pointsPerLine) - , customPointSizeGroups(std::move(other.customPointSizeGroups)) - , hasCustomPointSizes(other.hasCustomPointSizes) - , vertexBuffer(QOpenGLBuffer::VertexBuffer) - , colorBuffer(QOpenGLBuffer::VertexBuffer) - , vboCreated(false) - { - // VBO 需要在 GL 上下文中重建,标记为未创建 - other.vboCreated = false; - } - - PointCloudData& operator=(PointCloudData&& other) noexcept - { - if (this != &other) { - // 释放自身 VBO - if (vboCreated) { - vertexBuffer.destroy(); - colorBuffer.destroy(); - vboCreated = false; - } - vertices = std::move(other.vertices); - colors = std::move(other.colors); - lineIndices = std::move(other.lineIndices); - originalIndices = std::move(other.originalIndices); - pointInLineIndices = std::move(other.pointInLineIndices); - hasColor = other.hasColor; - hasLineInfo = other.hasLineInfo; - name = std::move(other.name); - colorIndex = other.colorIndex; - totalLines = other.totalLines; - pointsPerLine = other.pointsPerLine; - customPointSizeGroups = std::move(other.customPointSizeGroups); - hasCustomPointSizes = other.hasCustomPointSizes; - // VBO 需要重建 - other.vboCreated = false; - } - return *this; - } - - // 禁止拷贝 - PointCloudData(const PointCloudData&) = delete; - PointCloudData& operator=(const PointCloudData&) = delete; - }; - - void computeBoundingBox(); - void setCurrentColor(PointCloudColor color); - void setColorByIndex(int colorIndex); // 根据索引设置颜色 - SelectedPointInfo pickPoint(int screenX, int screenY); - void drawSelectedPoints(); - void drawMeasurementLine(); - void drawAxis(); - void drawSelectedLine(); // 绘制选中的线 - void drawLineSegments(); // 绘制线段 - void drawBasicShapeSurfaces(); // 绘制基础图形面片 - void drawBasicShapeSegments(); // 绘制基础图形 - void drawLineSegmentList(const QVector& segments); - void drawPosePoints(); // 绘制姿态点 - void drawAxisLabels(); // 绘制坐标轴 XYZ 标注 - void uploadToVBO(PointCloudData& data); // 上传数据到 VBO - void releaseVBO(PointCloudData& data); // 释放 VBO 资源 - - std::vector m_pointClouds; - - QMatrix4x4 m_projection; - QMatrix4x4 m_view; - QMatrix4x4 m_model; - - float m_distance; - float m_rotationX; - float m_rotationY; - float m_rotationZ; - QQuaternion m_rotation; // 使用四元数存储旋转状态 - QVector3D m_center; - QVector3D m_pan; - QVector3D m_minBound; - QVector3D m_maxBound; - - QPoint m_lastMousePos; - bool m_leftButtonPressed; - bool m_rightButtonPressed; - bool m_middleButtonPressed; - - PointCloudColor m_currentColor; - float m_pointSize; - - LineSelectMode m_lineSelectMode; - EulerRotationOrder m_eulerRotationOrder; - bool m_measureDistanceEnabled; - QVector m_selectedPoints; - SelectedLineInfo m_selectedLine; - static const int MAX_SELECTED_POINTS = 2; - - // 列表高亮点(与选点功能区分) - bool m_hasListHighlightPoint; - QVector3D m_listHighlightPoint; - - int m_colorIndex; // 颜色轮换索引 - static const int COLOR_COUNT = 7; // 可用颜色数量 - - // 线段和姿态点数据 - QVector m_lineSegments; - QVector m_basicShapeSegments; - QVector m_basicShapeSurfaces; - QVector m_posePoints; -}; - -#endif // POINT_CLOUD_GL_WIDGET_H +#endif // CLOUD_VIEW_POINT_CLOUD_GL_WIDGET_FORWARDER_H diff --git a/CloudView/Src/CloudViewMainWindow.cpp b/CloudView/Src/CloudViewMainWindow.cpp index a6cc749..9dd45d8 100644 --- a/CloudView/Src/CloudViewMainWindow.cpp +++ b/CloudView/Src/CloudViewMainWindow.cpp @@ -1,408 +1,422 @@ -#include "CloudViewMainWindow.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include "CloudViewMainWindow.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include #include #include #include #include -#include -#include -#include -#include -#include "VrLog.h" -#include "LaserDataLoader.h" - -namespace -{ +#include +#include +#include +#include +#include "VrLog.h" +#include "LaserDataLoader.h" + +namespace +{ constexpr double kPi = 3.14159265358979323846; constexpr double kDegToRad = kPi / 180.0; - -bool parseMatrixText(const QString& text, QMatrix4x4& matrix, QString* errorMessage = nullptr) -{ - QStringList lines = text.split('\n', QString::SkipEmptyParts); - QVector> rows; - - for (const QString& line : lines) { - QString cleaned = line.trimmed(); - if (cleaned.isEmpty() || cleaned.startsWith('#')) { - continue; - } - - cleaned.replace(',', ' '); - cleaned.replace('\t', ' '); - const QStringList parts = cleaned.split(' ', QString::SkipEmptyParts); - - QVector row; - bool ok = true; - for (const QString& part : parts) { - const float value = part.toFloat(&ok); - if (!ok) { - break; - } - row.append(value); - } - - if (!ok || row.size() != 4) { - if (errorMessage) { - *errorMessage = QString("矩阵格式无效,第 %1 行需要 4 个数值").arg(rows.size() + 1); - } - return false; - } - - rows.append(row); - } - - if (rows.size() != 4) { - if (errorMessage) { - *errorMessage = QString("矩阵需要 4 行数值,当前为 %1 行").arg(rows.size()); - } - return false; - } - - matrix.setToIdentity(); - for (int row = 0; row < 4; ++row) { - for (int col = 0; col < 4; ++col) { - matrix(row, col) = rows[row][col]; - } - } - return true; -} - -QString formatMatrixText(const QMatrix4x4& matrix) -{ - QStringList lines; - for (int row = 0; row < 4; ++row) { - QStringList values; - for (int col = 0; col < 4; ++col) { - values.append(QString::number(matrix(row, col), 'f', 6)); - } - lines.append(values.join(" ")); - } - return lines.join("\n"); -} - -// 从 EyeHandCalibMatrixInfo.ini 格式加载标定矩阵 -// 返回 true 表示成功,matrixList 填充所有找到的矩阵及其描述信息 -bool parseCalibIniFile(const QString& fileName, QVector>& matrixList, QString* errorMessage = nullptr) -{ - QSettings settings(fileName, QSettings::IniFormat); - - const int nExist = settings.value("CommInfo/nExistMatrixNum", 0).toInt(); - if (nExist <= 0) { - if (errorMessage) *errorMessage = "INI 文件中未找到标定矩阵 (nExistMatrixNum=0)"; - return false; - } - - for (int i = 0; i < nExist; ++i) { - const QString section = QString("CalibMatrixInfo_%1").arg(i); - settings.beginGroup(section); - - QMatrix4x4 matrix; - matrix.setToIdentity(); - bool ok = true; - for (int idx = 0; idx < 16; ++idx) { - const QString key = QString("dCalibMatrix_%1").arg(idx); - const double val = settings.value(key, 0.0).toDouble(&ok); - if (!ok) break; - const int row = idx / 4; - const int col = idx % 4; - matrix(row, col) = static_cast(val); - } - - if (!ok) { - settings.endGroup(); - if (errorMessage) *errorMessage = QString("矩阵 %1 的数据格式无效").arg(i); - return false; - } - - const int posIdx = settings.value("nCalibPosIdx", i).toInt(); - const QString posName = settings.value("sCalibPosName", "").toString(); - const QString calibTime = settings.value("sCalibTime", "").toString(); - QString desc = QString("矩阵 %1 (位置%2").arg(i).arg(posIdx); - if (!posName.isEmpty()) desc += QString(", %1").arg(posName); - if (!calibTime.isEmpty()) desc += QString(", %1").arg(calibTime); - desc += ")"; - - settings.endGroup(); - matrixList.append(qMakePair(desc, matrix)); - } - return true; -} - -QMatrix4x4 makeAxisRotationMatrix(char axis, double angleRad) -{ - QMatrix4x4 matrix; - matrix.setToIdentity(); - - const float c = static_cast(std::cos(angleRad)); - const float s = static_cast(std::sin(angleRad)); - - switch (axis) { - case 'x': - matrix(1, 1) = c; - matrix(1, 2) = -s; - matrix(2, 1) = s; - matrix(2, 2) = c; - break; - case 'y': - matrix(0, 0) = c; - matrix(0, 2) = s; - matrix(2, 0) = -s; - matrix(2, 2) = c; - break; - case 'z': - matrix(0, 0) = c; - matrix(0, 1) = -s; - matrix(1, 0) = s; - matrix(1, 1) = c; - break; - default: - break; - } - - return matrix; -} - -QMatrix4x4 buildRobotPoseMatrix(float x, float y, float z, float rxDeg, float ryDeg, float rzDeg) -{ - const QMatrix4x4 rotation = - makeAxisRotationMatrix('z', rzDeg * kDegToRad) * - makeAxisRotationMatrix('y', ryDeg * kDegToRad) * - makeAxisRotationMatrix('x', rxDeg * kDegToRad); - - QMatrix4x4 matrix = rotation; - matrix(0, 3) = x; - matrix(1, 3) = y; - matrix(2, 3) = z; - matrix(3, 0) = 0.0f; - matrix(3, 1) = 0.0f; - matrix(3, 2) = 0.0f; - matrix(3, 3) = 1.0f; - return matrix; -} - -void transformPointCloud(PointCloudXYZ& cloud, const QMatrix4x4& matrix) -{ - for (auto& point : cloud.points) { - const QVector3D transformed = matrix.map(QVector3D(point.x, point.y, point.z)); - point.x = transformed.x(); - point.y = transformed.y(); - point.z = transformed.z(); - } -} - -double clampValue(double value, double minValue, double maxValue) -{ - return std::max(minValue, std::min(maxValue, value)); -} - -QMatrix4x4 buildEulerRotationMatrix(EulerRotationOrder order, double rxDeg, double ryDeg, double rzDeg) -{ - const QMatrix4x4 Rx = makeAxisRotationMatrix('x', rxDeg * kDegToRad); - const QMatrix4x4 Ry = makeAxisRotationMatrix('y', ryDeg * kDegToRad); - const QMatrix4x4 Rz = makeAxisRotationMatrix('z', rzDeg * kDegToRad); - - switch (order) { - case EulerRotationOrder::ZYX: return Rz * Ry * Rx; - case EulerRotationOrder::XYZ: return Rx * Ry * Rz; - case EulerRotationOrder::ZXY: return Rz * Rx * Ry; - case EulerRotationOrder::YXZ: return Ry * Rx * Rz; - case EulerRotationOrder::XZY: return Rx * Rz * Ry; - case EulerRotationOrder::YZX: return Ry * Rz * Rx; - } - - QMatrix4x4 identity; - identity.setToIdentity(); - return identity; -} - -QVector3D rotationMatrixToEuler(EulerRotationOrder order, const QMatrix4x4& m) -{ - constexpr double kEps = 1e-8; - double rx = 0.0; - double ry = 0.0; - double rz = 0.0; - - switch (order) { - case EulerRotationOrder::ZYX: { - const double cy = std::sqrt(m(0, 0) * m(0, 0) + m(1, 0) * m(1, 0)); - if (cy > kEps) { - rx = std::atan2(m(2, 1), m(2, 2)); - ry = std::atan2(-m(2, 0), cy); - rz = std::atan2(m(1, 0), m(0, 0)); - } else { - rx = std::atan2(-m(1, 2), m(1, 1)); - ry = std::atan2(-m(2, 0), cy); - rz = 0.0; - } - break; - } - case EulerRotationOrder::XYZ: { - const double cy = std::sqrt(m(0, 0) * m(0, 0) + m(0, 1) * m(0, 1)); - if (cy > kEps) { - rx = std::atan2(-m(1, 2), m(2, 2)); - ry = std::atan2(m(0, 2), cy); - rz = std::atan2(-m(0, 1), m(0, 0)); - } else { - rx = std::atan2(m(2, 1), m(1, 1)); - ry = std::atan2(m(0, 2), cy); - rz = 0.0; - } - break; - } - case EulerRotationOrder::ZXY: { - const double cx = std::sqrt(m(2, 0) * m(2, 0) + m(2, 2) * m(2, 2)); - if (cx > kEps) { - rx = std::atan2(m(2, 1), cx); - ry = std::atan2(-m(2, 0), m(2, 2)); - rz = std::atan2(-m(0, 1), m(1, 1)); - } else { - rx = std::atan2(m(2, 1), cx); - ry = 0.0; - rz = std::atan2(m(1, 0), m(0, 0)); - } - break; - } - case EulerRotationOrder::YXZ: { - const double cx = std::sqrt(m(1, 0) * m(1, 0) + m(1, 1) * m(1, 1)); - if (cx > kEps) { - rx = std::atan2(-m(1, 2), cx); - ry = std::atan2(m(0, 2), m(2, 2)); - rz = std::atan2(m(1, 0), m(1, 1)); - } else { - rx = std::atan2(-m(1, 2), cx); - ry = std::atan2(-m(2, 0), m(0, 0)); - rz = 0.0; - } - break; - } - case EulerRotationOrder::XZY: { - const double cz = std::sqrt(m(0, 0) * m(0, 0) + m(0, 2) * m(0, 2)); - if (cz > kEps) { - rx = std::atan2(m(2, 1), m(1, 1)); - ry = std::atan2(m(0, 2), m(0, 0)); - rz = std::atan2(-m(0, 1), cz); - } else { - rx = std::atan2(-m(1, 2), m(2, 2)); - ry = 0.0; - rz = std::atan2(-m(0, 1), cz); - } - break; - } - case EulerRotationOrder::YZX: { - const double cz = std::sqrt(m(1, 1) * m(1, 1) + m(1, 2) * m(1, 2)); - if (cz > kEps) { - rx = std::atan2(-m(1, 2), m(1, 1)); - ry = std::atan2(-m(2, 0), m(0, 0)); - rz = std::atan2(m(1, 0), cz); - } else { - rx = 0.0; - ry = std::atan2(m(0, 2), m(2, 2)); - rz = std::atan2(m(1, 0), cz); - } - break; - } - } - - return QVector3D(static_cast(rx / kDegToRad), - static_cast(ry / kDegToRad), - static_cast(rz / kDegToRad)); -} - -PointCloudXYZ generateBoxPointCloud(const QVector3D& center, const QVector3D& size, float spacing) -{ - PointCloudXYZ cloud; - - const int xSteps = std::max(1, static_cast(std::ceil(size.x() / spacing))); - const int ySteps = std::max(1, static_cast(std::ceil(size.y() / spacing))); - const int zSteps = std::max(1, static_cast(std::ceil(size.z() / spacing))); - - const float stepX = size.x() / xSteps; - const float stepY = size.y() / ySteps; - const float stepZ = size.z() / zSteps; - - cloud.reserve(static_cast((xSteps + 1) * (ySteps + 1) * (zSteps + 1))); - - const float minX = center.x() - size.x() * 0.5f; - const float minY = center.y() - size.y() * 0.5f; - const float minZ = center.z() - size.z() * 0.5f; - - for (int ix = 0; ix <= xSteps; ++ix) { - const bool onXBoundary = (ix == 0 || ix == xSteps); - const float x = minX + stepX * ix; - for (int iy = 0; iy <= ySteps; ++iy) { - const bool onYBoundary = (iy == 0 || iy == ySteps); - const float y = minY + stepY * iy; - for (int iz = 0; iz <= zSteps; ++iz) { - const bool onZBoundary = (iz == 0 || iz == zSteps); - if (!(onXBoundary || onYBoundary || onZBoundary)) { - continue; - } - - const float z = minZ + stepZ * iz; - cloud.push_back(Point3D(x, y, z)); - } - } - } - - return cloud; -} - -void getShapeFrame(const PointCloudXYZ& cloud, QVector3D& center, QVector3D& size) -{ - center = QVector3D(0.0f, 0.0f, 0.0f); - size = QVector3D(100.0f, 100.0f, 100.0f); - - if (cloud.empty()) { - return; - } - - QVector3D minBound(FLT_MAX, FLT_MAX, FLT_MAX); - QVector3D maxBound(-FLT_MAX, -FLT_MAX, -FLT_MAX); - for (const auto& point : cloud.points) { - minBound.setX(std::min(minBound.x(), point.x)); - minBound.setY(std::min(minBound.y(), point.y)); - minBound.setZ(std::min(minBound.z(), point.z)); - maxBound.setX(std::max(maxBound.x(), point.x)); - maxBound.setY(std::max(maxBound.y(), point.y)); - maxBound.setZ(std::max(maxBound.z(), point.z)); - } - - center = (minBound + maxBound) * 0.5f; - size = maxBound - minBound; -} - +constexpr float kCircleR = 1.0f; +constexpr float kCircleG = 0.85f; +constexpr float kCircleB = 0.0f; +constexpr float kRectangleR = 1.0f; +constexpr float kRectangleG = 0.25f; +constexpr float kRectangleB = 0.9f; +constexpr float kSurfaceR = 0.0f; +constexpr float kSurfaceG = 0.8f; +constexpr float kSurfaceB = 1.0f; +constexpr float kSurfaceA = 0.5f; +constexpr float kShapeCenterR = 1.0f; +constexpr float kShapeCenterG = 0.1f; +constexpr float kShapeCenterB = 0.1f; +constexpr float kShapeCenterPointSize = 9.0f; + +bool parseMatrixText(const QString& text, QMatrix4x4& matrix, QString* errorMessage = nullptr) +{ + QStringList lines = text.split('\n', QString::SkipEmptyParts); + QVector> rows; + + for (const QString& line : lines) { + QString cleaned = line.trimmed(); + if (cleaned.isEmpty() || cleaned.startsWith('#')) { + continue; + } + + cleaned.replace(',', ' '); + cleaned.replace('\t', ' '); + const QStringList parts = cleaned.split(' ', QString::SkipEmptyParts); + + QVector row; + bool ok = true; + for (const QString& part : parts) { + const float value = part.toFloat(&ok); + if (!ok) { + break; + } + row.append(value); + } + + if (!ok || row.size() != 4) { + if (errorMessage) { + *errorMessage = QString("矩阵格式无效,第 %1 行需要 4 个数值").arg(rows.size() + 1); + } + return false; + } + + rows.append(row); + } + + if (rows.size() != 4) { + if (errorMessage) { + *errorMessage = QString("矩阵需要 4 行数值,当前为 %1 行").arg(rows.size()); + } + return false; + } + + matrix.setToIdentity(); + for (int row = 0; row < 4; ++row) { + for (int col = 0; col < 4; ++col) { + matrix(row, col) = rows[row][col]; + } + } + return true; +} + +QString formatMatrixText(const QMatrix4x4& matrix) +{ + QStringList lines; + for (int row = 0; row < 4; ++row) { + QStringList values; + for (int col = 0; col < 4; ++col) { + values.append(QString::number(matrix(row, col), 'f', 6)); + } + lines.append(values.join(" ")); + } + return lines.join("\n"); +} + +// 从 EyeHandCalibMatrixInfo.ini 格式加载标定矩阵 +// 返回 true 表示成功,matrixList 填充所有找到的矩阵及其描述信息 +bool parseCalibIniFile(const QString& fileName, QVector>& matrixList, QString* errorMessage = nullptr) +{ + QSettings settings(fileName, QSettings::IniFormat); + + const int nExist = settings.value("CommInfo/nExistMatrixNum", 0).toInt(); + if (nExist <= 0) { + if (errorMessage) *errorMessage = "INI 文件中未找到标定矩阵 (nExistMatrixNum=0)"; + return false; + } + + for (int i = 0; i < nExist; ++i) { + const QString section = QString("CalibMatrixInfo_%1").arg(i); + settings.beginGroup(section); + + QMatrix4x4 matrix; + matrix.setToIdentity(); + bool ok = true; + for (int idx = 0; idx < 16; ++idx) { + const QString key = QString("dCalibMatrix_%1").arg(idx); + const double val = settings.value(key, 0.0).toDouble(&ok); + if (!ok) break; + const int row = idx / 4; + const int col = idx % 4; + matrix(row, col) = static_cast(val); + } + + if (!ok) { + settings.endGroup(); + if (errorMessage) *errorMessage = QString("矩阵 %1 的数据格式无效").arg(i); + return false; + } + + const int posIdx = settings.value("nCalibPosIdx", i).toInt(); + const QString posName = settings.value("sCalibPosName", "").toString(); + const QString calibTime = settings.value("sCalibTime", "").toString(); + QString desc = QString("矩阵 %1 (位置%2").arg(i).arg(posIdx); + if (!posName.isEmpty()) desc += QString(", %1").arg(posName); + if (!calibTime.isEmpty()) desc += QString(", %1").arg(calibTime); + desc += ")"; + + settings.endGroup(); + matrixList.append(qMakePair(desc, matrix)); + } + return true; +} + +QMatrix4x4 makeAxisRotationMatrix(char axis, double angleRad) +{ + QMatrix4x4 matrix; + matrix.setToIdentity(); + + const float c = static_cast(std::cos(angleRad)); + const float s = static_cast(std::sin(angleRad)); + + switch (axis) { + case 'x': + matrix(1, 1) = c; + matrix(1, 2) = -s; + matrix(2, 1) = s; + matrix(2, 2) = c; + break; + case 'y': + matrix(0, 0) = c; + matrix(0, 2) = s; + matrix(2, 0) = -s; + matrix(2, 2) = c; + break; + case 'z': + matrix(0, 0) = c; + matrix(0, 1) = -s; + matrix(1, 0) = s; + matrix(1, 1) = c; + break; + default: + break; + } + + return matrix; +} + +QMatrix4x4 buildRobotPoseMatrix(float x, float y, float z, float rxDeg, float ryDeg, float rzDeg) +{ + const QMatrix4x4 rotation = + makeAxisRotationMatrix('z', rzDeg * kDegToRad) * + makeAxisRotationMatrix('y', ryDeg * kDegToRad) * + makeAxisRotationMatrix('x', rxDeg * kDegToRad); + + QMatrix4x4 matrix = rotation; + matrix(0, 3) = x; + matrix(1, 3) = y; + matrix(2, 3) = z; + matrix(3, 0) = 0.0f; + matrix(3, 1) = 0.0f; + matrix(3, 2) = 0.0f; + matrix(3, 3) = 1.0f; + return matrix; +} + +void transformPointCloud(PointCloudXYZ& cloud, const QMatrix4x4& matrix) +{ + for (auto& point : cloud.points) { + const QVector3D transformed = matrix.map(QVector3D(point.x, point.y, point.z)); + point.x = transformed.x(); + point.y = transformed.y(); + point.z = transformed.z(); + } +} + +double clampValue(double value, double minValue, double maxValue) +{ + return std::max(minValue, std::min(maxValue, value)); +} + +QMatrix4x4 buildEulerRotationMatrix(EulerRotationOrder order, double rxDeg, double ryDeg, double rzDeg) +{ + const QMatrix4x4 Rx = makeAxisRotationMatrix('x', rxDeg * kDegToRad); + const QMatrix4x4 Ry = makeAxisRotationMatrix('y', ryDeg * kDegToRad); + const QMatrix4x4 Rz = makeAxisRotationMatrix('z', rzDeg * kDegToRad); + + switch (order) { + case EulerRotationOrder::ZYX: return Rz * Ry * Rx; + case EulerRotationOrder::XYZ: return Rx * Ry * Rz; + case EulerRotationOrder::ZXY: return Rz * Rx * Ry; + case EulerRotationOrder::YXZ: return Ry * Rx * Rz; + case EulerRotationOrder::XZY: return Rx * Rz * Ry; + case EulerRotationOrder::YZX: return Ry * Rz * Rx; + } + + QMatrix4x4 identity; + identity.setToIdentity(); + return identity; +} + +QVector3D rotationMatrixToEuler(EulerRotationOrder order, const QMatrix4x4& m) +{ + constexpr double kEps = 1e-8; + double rx = 0.0; + double ry = 0.0; + double rz = 0.0; + + switch (order) { + case EulerRotationOrder::ZYX: { + const double cy = std::sqrt(m(0, 0) * m(0, 0) + m(1, 0) * m(1, 0)); + if (cy > kEps) { + rx = std::atan2(m(2, 1), m(2, 2)); + ry = std::atan2(-m(2, 0), cy); + rz = std::atan2(m(1, 0), m(0, 0)); + } else { + rx = std::atan2(-m(1, 2), m(1, 1)); + ry = std::atan2(-m(2, 0), cy); + rz = 0.0; + } + break; + } + case EulerRotationOrder::XYZ: { + const double cy = std::sqrt(m(0, 0) * m(0, 0) + m(0, 1) * m(0, 1)); + if (cy > kEps) { + rx = std::atan2(-m(1, 2), m(2, 2)); + ry = std::atan2(m(0, 2), cy); + rz = std::atan2(-m(0, 1), m(0, 0)); + } else { + rx = std::atan2(m(2, 1), m(1, 1)); + ry = std::atan2(m(0, 2), cy); + rz = 0.0; + } + break; + } + case EulerRotationOrder::ZXY: { + const double cx = std::sqrt(m(2, 0) * m(2, 0) + m(2, 2) * m(2, 2)); + if (cx > kEps) { + rx = std::atan2(m(2, 1), cx); + ry = std::atan2(-m(2, 0), m(2, 2)); + rz = std::atan2(-m(0, 1), m(1, 1)); + } else { + rx = std::atan2(m(2, 1), cx); + ry = 0.0; + rz = std::atan2(m(1, 0), m(0, 0)); + } + break; + } + case EulerRotationOrder::YXZ: { + const double cx = std::sqrt(m(1, 0) * m(1, 0) + m(1, 1) * m(1, 1)); + if (cx > kEps) { + rx = std::atan2(-m(1, 2), cx); + ry = std::atan2(m(0, 2), m(2, 2)); + rz = std::atan2(m(1, 0), m(1, 1)); + } else { + rx = std::atan2(-m(1, 2), cx); + ry = std::atan2(-m(2, 0), m(0, 0)); + rz = 0.0; + } + break; + } + case EulerRotationOrder::XZY: { + const double cz = std::sqrt(m(0, 0) * m(0, 0) + m(0, 2) * m(0, 2)); + if (cz > kEps) { + rx = std::atan2(m(2, 1), m(1, 1)); + ry = std::atan2(m(0, 2), m(0, 0)); + rz = std::atan2(-m(0, 1), cz); + } else { + rx = std::atan2(-m(1, 2), m(2, 2)); + ry = 0.0; + rz = std::atan2(-m(0, 1), cz); + } + break; + } + case EulerRotationOrder::YZX: { + const double cz = std::sqrt(m(1, 1) * m(1, 1) + m(1, 2) * m(1, 2)); + if (cz > kEps) { + rx = std::atan2(-m(1, 2), m(1, 1)); + ry = std::atan2(-m(2, 0), m(0, 0)); + rz = std::atan2(m(1, 0), cz); + } else { + rx = 0.0; + ry = std::atan2(m(0, 2), m(2, 2)); + rz = std::atan2(m(1, 0), cz); + } + break; + } + } + + return QVector3D(static_cast(rx / kDegToRad), + static_cast(ry / kDegToRad), + static_cast(rz / kDegToRad)); +} + +PointCloudXYZ generateBoxPointCloud(const QVector3D& center, const QVector3D& size, float spacing) +{ + PointCloudXYZ cloud; + + const int xSteps = std::max(1, static_cast(std::ceil(size.x() / spacing))); + const int ySteps = std::max(1, static_cast(std::ceil(size.y() / spacing))); + const int zSteps = std::max(1, static_cast(std::ceil(size.z() / spacing))); + + const float stepX = size.x() / xSteps; + const float stepY = size.y() / ySteps; + const float stepZ = size.z() / zSteps; + + cloud.reserve(static_cast((xSteps + 1) * (ySteps + 1) * (zSteps + 1))); + + const float minX = center.x() - size.x() * 0.5f; + const float minY = center.y() - size.y() * 0.5f; + const float minZ = center.z() - size.z() * 0.5f; + + for (int ix = 0; ix <= xSteps; ++ix) { + const bool onXBoundary = (ix == 0 || ix == xSteps); + const float x = minX + stepX * ix; + for (int iy = 0; iy <= ySteps; ++iy) { + const bool onYBoundary = (iy == 0 || iy == ySteps); + const float y = minY + stepY * iy; + for (int iz = 0; iz <= zSteps; ++iz) { + const bool onZBoundary = (iz == 0 || iz == zSteps); + if (!(onXBoundary || onYBoundary || onZBoundary)) { + continue; + } + + const float z = minZ + stepZ * iz; + cloud.push_back(Point3D(x, y, z)); + } + } + } + + return cloud; +} + +void getShapeFrame(const PointCloudXYZ& cloud, QVector3D& center, QVector3D& size) +{ + center = QVector3D(0.0f, 0.0f, 0.0f); + size = QVector3D(100.0f, 100.0f, 100.0f); + + if (cloud.empty()) { + return; + } + + QVector3D minBound(FLT_MAX, FLT_MAX, FLT_MAX); + QVector3D maxBound(-FLT_MAX, -FLT_MAX, -FLT_MAX); + for (const auto& point : cloud.points) { + minBound.setX(std::min(minBound.x(), point.x)); + minBound.setY(std::min(minBound.y(), point.y)); + minBound.setZ(std::min(minBound.z(), point.z)); + maxBound.setX(std::max(maxBound.x(), point.x)); + maxBound.setY(std::max(maxBound.y(), point.y)); + maxBound.setZ(std::max(maxBound.z(), point.z)); + } + + center = (minBound + maxBound) * 0.5f; + size = maxBound - minBound; +} + QDoubleSpinBox* createShapeSpinBox(QWidget* parent, double value, double minValue = -1000000000.0, double maxValue = 1000000000.0) { QDoubleSpinBox* spin = new QDoubleSpinBox(parent); spin->setDecimals(6); - spin->setRange(minValue, maxValue); + spin->setRange(minValue, maxValue); spin->setValue(value); spin->setSingleStep(1.0); spin->setAlignment(Qt::AlignRight); @@ -428,19 +442,32 @@ QVector makeClosedShapeSegments(const QVector& points, f QVector segments; if (points.size() < 2) { return segments; - } - - segments.reserve(points.size()); - for (int i = 0; i < points.size(); ++i) { - const QVector3D& p1 = points[i]; - const QVector3D& p2 = points[(i + 1) % points.size()]; - segments.append(LineSegment(p1.x(), p1.y(), p1.z(), - p2.x(), p2.y(), p2.z(), - r, g, b, 3.0f)); + } + + segments.reserve(points.size()); + for (int i = 0; i < points.size(); ++i) { + const QVector3D& p1 = points[i]; + const QVector3D& p2 = points[(i + 1) % points.size()]; + segments.append(LineSegment(p1.x(), p1.y(), p1.z(), + p2.x(), p2.y(), p2.z(), + r, g, b, 3.0f)); } return segments; } +QVector makeShapeCenterPoint(const QVector3D& center) +{ + QVector points; + points.append(BasicShapePoint(center.x(), + center.y(), + center.z(), + kShapeCenterR, + kShapeCenterG, + kShapeCenterB, + kShapeCenterPointSize)); + return points; +} + QVector makeCircleSegments(const QVector3D& center, double rxDeg, double ryDeg, @@ -466,7 +493,7 @@ QVector makeCircleSegments(const QVector3D& center, segments.append(LineSegment( p1.x(), p1.y(), p1.z(), p2.x(), p2.y(), p2.z(), - 1.0f, 0.85f, 0.0f, 3.0f)); + kCircleR, kCircleG, kCircleB, 3.0f)); } return segments; } @@ -493,7 +520,7 @@ QVector makeRectangleSegments(const QVector3D& center, points.append(transformShapePoint(center, rotation, QVector3D( halfWidth, -halfHeight, 0.0f))); points.append(transformShapePoint(center, rotation, QVector3D( halfWidth, halfHeight, 0.0f))); points.append(transformShapePoint(center, rotation, QVector3D(-halfWidth, halfHeight, 0.0f))); - return makeClosedShapeSegments(points, 0.0f, 0.8f, 1.0f); + return makeClosedShapeSegments(points, kRectangleR, kRectangleG, kRectangleB); } QVector makeSurfacePoints(const QVector3D& center, @@ -535,7 +562,7 @@ QVector makeSurfaceQuads(const QVector3D& center, return surfaces; } - surfaces.append(SurfaceQuad(points[0], points[1], points[2], points[3], 0.0f, 0.8f, 1.0f, 0.5f)); + surfaces.append(SurfaceQuad(points[0], points[1], points[2], points[3], kSurfaceR, kSurfaceG, kSurfaceB, kSurfaceA)); return surfaces; } @@ -548,7 +575,7 @@ QVector makeSurfaceOutlineSegments(const QVector3D& center, EulerRotationOrder order) { const QVector points = makeSurfacePoints(center, rxDeg, ryDeg, rzDeg, length, width, order); - return makeClosedShapeSegments(points, 0.0f, 0.8f, 1.0f); + return makeClosedShapeSegments(points, kSurfaceR, kSurfaceG, kSurfaceB); } void appendLineSegments(QVector& target, const QVector& source) @@ -558,6 +585,13 @@ void appendLineSegments(QVector& target, const QVector } } +void appendBasicShapePoints(QVector& target, const QVector& source) +{ + for (const BasicShapePoint& point : source) { + target.append(point); + } +} + void appendSurfaceQuads(QVector& target, const QVector& source) { for (const SurfaceQuad& surface : source) { @@ -628,11 +662,13 @@ bool loadNumericRowsFromFile(const QString& fileName, QVector& rows, bool makeCircleSegmentsFromRows(const QVector& rows, EulerRotationOrder order, QVector& segments, + QVector& centerPoints, int& shapeCount, QString& errorMessage) { constexpr int kRecordSize = 7; segments.clear(); + centerPoints.clear(); shapeCount = 0; if (rows.isEmpty()) { @@ -660,6 +696,7 @@ bool makeCircleSegmentsFromRows(const QVector& rows, values[5], static_cast(values[6]), order)); + appendBasicShapePoints(centerPoints, makeShapeCenterPoint(center)); ++shapeCount; } @@ -669,11 +706,13 @@ bool makeCircleSegmentsFromRows(const QVector& rows, bool makeRectangleSegmentsFromRows(const QVector& rows, EulerRotationOrder order, QVector& segments, + QVector& centerPoints, int& shapeCount, QString& errorMessage) { constexpr int kRecordSize = 8; segments.clear(); + centerPoints.clear(); shapeCount = 0; if (rows.isEmpty()) { @@ -702,6 +741,7 @@ bool makeRectangleSegmentsFromRows(const QVector& rows, static_cast(values[6]), static_cast(values[7]), order)); + appendBasicShapePoints(centerPoints, makeShapeCenterPoint(center)); ++shapeCount; } @@ -790,23 +830,24 @@ bool makeTriangleSegmentsFromRows(const QVector& rows, return shapeCount > 0; } } // namespace - -CloudViewMainWindow::CloudViewMainWindow(QWidget* parent) + +CloudViewMainWindow::CloudViewMainWindow(QWidget* parent) : QMainWindow(parent) , m_glWidget(nullptr) , m_converter(std::make_unique()) + , m_lblDeltaXYZ(nullptr) , m_cloudCount(0) - , m_currentLineNum(0) + , m_currentLineNum(0) , m_currentLinePtNum(0) , m_linePointsDialog(nullptr) , m_linePointsTable(nullptr) , m_startupSequenceIndex(-1) , m_startupSequentialMode(false) { - setupUI(); - LOG_INFO("CloudViewMainWindow initialized\n"); -} - + setupUI(); + LOG_INFO("CloudViewMainWindow initialized\n"); +} + CloudViewMainWindow::~CloudViewMainWindow() { } @@ -974,15 +1015,16 @@ bool CloudViewMainWindow::loadCircleShapeFile(const QString& fileName) } QVector segments; + QVector centerPoints; int shapeCount = 0; - if (!makeCircleSegmentsFromRows(rows, currentShapeEulerOrder(m_comboEulerOrder), segments, shapeCount, errorMessage)) { + if (!makeCircleSegmentsFromRows(rows, currentShapeEulerOrder(m_comboEulerOrder), segments, centerPoints, shapeCount, errorMessage)) { statusBar()->showMessage(QString("加载圆数据失败:%1").arg(errorMessage)); LOG_WARN("[CloudView] Parse circle file failed: %s, %s\n", fileName.toStdString().c_str(), errorMessage.toStdString().c_str()); return false; } - showBasicShape(QString("%1 个圆").arg(shapeCount), segments); + showBasicShape(QString("%1 个圆").arg(shapeCount), segments, centerPoints); m_cloudList->addItem(QString("Circle (%1) - %2 个").arg(QFileInfo(fileName).fileName()).arg(shapeCount)); return true; } @@ -999,15 +1041,16 @@ bool CloudViewMainWindow::loadRectangleShapeFile(const QString& fileName) } QVector segments; + QVector centerPoints; int shapeCount = 0; - if (!makeRectangleSegmentsFromRows(rows, currentShapeEulerOrder(m_comboEulerOrder), segments, shapeCount, errorMessage)) { + if (!makeRectangleSegmentsFromRows(rows, currentShapeEulerOrder(m_comboEulerOrder), segments, centerPoints, shapeCount, errorMessage)) { statusBar()->showMessage(QString("加载矩形数据失败:%1").arg(errorMessage)); LOG_WARN("[CloudView] Parse rectangle file failed: %s, %s\n", fileName.toStdString().c_str(), errorMessage.toStdString().c_str()); return false; } - showBasicShape(QString("%1 个矩形").arg(shapeCount), segments); + showBasicShape(QString("%1 个矩形").arg(shapeCount), segments, centerPoints); m_cloudList->addItem(QString("Rectangle (%1) - %2 个").arg(QFileInfo(fileName).fileName()).arg(shapeCount)); return true; } @@ -1075,15 +1118,15 @@ bool CloudViewMainWindow::eventFilter(QObject* obj, QEvent* event) if (obj == m_glWidget && event->type() == QEvent::Resize) { auto* resizeEvt = static_cast(event); int w = resizeEvt->size().width(); - int h = resizeEvt->size().height(); - int btnW = m_btnZoomIn->width(); - int btnH = m_btnZoomIn->height(); - int margin = 8; - int gap = 4; - int x = w - btnW - margin; - int y = (h - btnH * 2 - gap) / 2; - m_btnZoomIn->move(x, y); - m_btnZoomOut->move(x, y + btnH + gap); + int h = resizeEvt->size().height(); + int btnW = m_btnZoomIn->width(); + int btnH = m_btnZoomIn->height(); + int margin = 8; + int gap = 4; + int x = w - btnW - margin; + int y = (h - btnH * 2 - gap) / 2; + m_btnZoomIn->move(x, y); + m_btnZoomOut->move(x, y + btnH + gap); } return QMainWindow::eventFilter(obj, event); } @@ -1104,1225 +1147,1241 @@ void CloudViewMainWindow::closeEvent(QCloseEvent* event) void CloudViewMainWindow::setupUI() { - // ── 文件菜单 ── - QMenu* fileMenu = menuBar()->addMenu("文件"); - QAction* actOpenFile = fileMenu->addAction("打开...\tCtrl+O"); - actOpenFile->setShortcut(QKeySequence("Ctrl+O")); - connect(actOpenFile, &QAction::triggered, this, &CloudViewMainWindow::onOpenFile); - fileMenu->addSeparator(); - QAction* actSaveCloud = fileMenu->addAction("保存txt点云...\tCtrl+S"); - actSaveCloud->setShortcut(QKeySequence("Ctrl+S")); - connect(actSaveCloud, &QAction::triggered, this, &CloudViewMainWindow::onSavePointCloud); - QAction* actSavePlyPcd = fileMenu->addAction("保存ply/pcd点云...\tCtrl+Shift+S"); - actSavePlyPcd->setShortcut(QKeySequence("Ctrl+Shift+S")); - connect(actSavePlyPcd, &QAction::triggered, this, &CloudViewMainWindow::onSavePlyPcdCloud); - fileMenu->addSeparator(); - QAction* actBatchConvert = fileMenu->addAction("批量转换txt->ply..."); - connect(actBatchConvert, &QAction::triggered, this, &CloudViewMainWindow::onBatchConvertTxtToPly); - - // ── 点云菜单 ── - QMenu* cloudMenu = menuBar()->addMenu("点云"); - QAction* actGenerateCloud = cloudMenu->addAction("生成长方体点云..."); - connect(actGenerateCloud, &QAction::triggered, this, &CloudViewMainWindow::onGeneratePointCloud); - QAction* actRotateByZ = cloudMenu->addAction("点云旋转..."); - connect(actRotateByZ, &QAction::triggered, this, &CloudViewMainWindow::onRotateCloudByZ); - - QMenu* toolMenu = menuBar()->addMenu("姿态工具"); - QAction* actEulerPose = toolMenu->addAction("欧拉角与方向向量矩阵互转..."); - connect(actEulerPose, &QAction::triggered, this, &CloudViewMainWindow::onConvertEulerMatrix); - QAction* actPoseComp = toolMenu->addAction("姿态补偿计算..."); - connect(actPoseComp, &QAction::triggered, this, &CloudViewMainWindow::onPoseCompensation); - + // ── 文件菜单 ── + QMenu* fileMenu = menuBar()->addMenu("文件"); + QAction* actOpenFile = fileMenu->addAction("打开...\tCtrl+O"); + actOpenFile->setShortcut(QKeySequence("Ctrl+O")); + connect(actOpenFile, &QAction::triggered, this, &CloudViewMainWindow::onOpenFile); + fileMenu->addSeparator(); + QAction* actSaveCloud = fileMenu->addAction("保存txt点云...\tCtrl+S"); + actSaveCloud->setShortcut(QKeySequence("Ctrl+S")); + connect(actSaveCloud, &QAction::triggered, this, &CloudViewMainWindow::onSavePointCloud); + QAction* actSavePlyPcd = fileMenu->addAction("保存ply/pcd点云...\tCtrl+Shift+S"); + actSavePlyPcd->setShortcut(QKeySequence("Ctrl+Shift+S")); + connect(actSavePlyPcd, &QAction::triggered, this, &CloudViewMainWindow::onSavePlyPcdCloud); + fileMenu->addSeparator(); + QAction* actBatchConvert = fileMenu->addAction("批量转换txt->ply..."); + connect(actBatchConvert, &QAction::triggered, this, &CloudViewMainWindow::onBatchConvertTxtToPly); + + // ── 点云菜单 ── + QMenu* cloudMenu = menuBar()->addMenu("点云"); + QAction* actGenerateCloud = cloudMenu->addAction("生成长方体点云..."); + connect(actGenerateCloud, &QAction::triggered, this, &CloudViewMainWindow::onGeneratePointCloud); + QAction* actRotateByZ = cloudMenu->addAction("点云旋转..."); + connect(actRotateByZ, &QAction::triggered, this, &CloudViewMainWindow::onRotateCloudByZ); + + QMenu* toolMenu = menuBar()->addMenu("姿态工具"); + QAction* actEulerPose = toolMenu->addAction("欧拉角与方向向量矩阵互转..."); + connect(actEulerPose, &QAction::triggered, this, &CloudViewMainWindow::onConvertEulerMatrix); + QAction* actPoseComp = toolMenu->addAction("姿态补偿计算..."); + connect(actPoseComp, &QAction::triggered, this, &CloudViewMainWindow::onPoseCompensation); + QMenu* imageToolMenu = menuBar()->addMenu("图形工具"); - QAction* actOpenPosePoint = imageToolMenu->addAction("点(带姿态)"); - connect(actOpenPosePoint, &QAction::triggered, this, &CloudViewMainWindow::onOpenPoseFile); - imageToolMenu->addSeparator(); - QAction* actCircleShape = imageToolMenu->addAction("圆"); - connect(actCircleShape, &QAction::triggered, this, &CloudViewMainWindow::onShowCircleShape); + QAction* actOpenPosePoint = imageToolMenu->addAction("点(带姿态)"); + connect(actOpenPosePoint, &QAction::triggered, this, &CloudViewMainWindow::onOpenPoseFile); + imageToolMenu->addSeparator(); + QAction* actCircleShape = imageToolMenu->addAction("圆"); + connect(actCircleShape, &QAction::triggered, this, &CloudViewMainWindow::onShowCircleShape); QAction* actRectangleShape = imageToolMenu->addAction("矩形"); connect(actRectangleShape, &QAction::triggered, this, &CloudViewMainWindow::onShowRectangleShape); QAction* actSurfaceShape = imageToolMenu->addAction("面"); connect(actSurfaceShape, &QAction::triggered, this, &CloudViewMainWindow::onShowSurfaceShape); QAction* actTriangleShape = imageToolMenu->addAction("三角形"); connect(actTriangleShape, &QAction::triggered, this, &CloudViewMainWindow::onShowTriangleShape); - - QMenu* transformMenu = menuBar()->addMenu("矩阵变换"); - QAction* actEyeToHand = transformMenu->addAction("眼在手外..."); - connect(actEyeToHand, &QAction::triggered, this, &CloudViewMainWindow::onEyeToHandTransform); - QAction* actEyeInHand = transformMenu->addAction("眼在手上..."); - connect(actEyeInHand, &QAction::triggered, this, &CloudViewMainWindow::onEyeInHandTransform); - // 创建中央控件 - QWidget* centralWidget = new QWidget(this); - setCentralWidget(centralWidget); - - // 创建主布局 - QHBoxLayout* mainLayout = new QHBoxLayout(centralWidget); - mainLayout->setContentsMargins(5, 5, 5, 5); - mainLayout->setSpacing(5); - - // 创建分割器 - QSplitter* splitter = new QSplitter(Qt::Horizontal, centralWidget); - - // 左侧:视图工具栏 + 点云显示区域 - QWidget* viewerContainer = new QWidget(splitter); - QHBoxLayout* viewerLayout = new QHBoxLayout(viewerContainer); - viewerLayout->setContentsMargins(0, 0, 0, 0); - viewerLayout->setSpacing(2); - - // 视图方向工具栏(纵向排列,在3D视图左侧) - QWidget* viewToolbar = createViewToolbar(); - viewerLayout->addWidget(viewToolbar); - - // 点云显示区域 - QWidget* viewerArea = createViewerArea(); - viewerLayout->addWidget(viewerArea); - - splitter->addWidget(viewerContainer); - - // 右侧:控制面板 - QWidget* controlPanel = createControlPanel(); - splitter->addWidget(controlPanel); - - // 设置分割器初始大小(左侧 70%,右侧 30%) - splitter->setSizes({700, 300}); - - mainLayout->addWidget(splitter); - - // 状态栏 - statusBar()->showMessage("就绪"); -} - -QWidget* CloudViewMainWindow::createViewerArea() -{ - QWidget* widget = new QWidget(this); - QVBoxLayout* layout = new QVBoxLayout(widget); - layout->setContentsMargins(0, 0, 0, 0); - - m_glWidget = new PointCloudGLWidget(widget); - m_glWidget->setMinimumSize(400, 300); - m_glWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - layout->addWidget(m_glWidget); - - // 悬浮缩放按钮(父控件为 m_glWidget,悬浮在右侧中间) - int zoomBtnSize = 28; - QString zoomBtnStyle = - "QToolButton {" - " background-color: rgba(50, 50, 50, 180);" - " color: white;" - " border: 1px solid rgba(120, 120, 120, 150);" - " border-radius: 4px;" - " font-size: 32px;" - " font-weight: bold;" - "}" - "QToolButton:hover {" - " background-color: rgba(80, 80, 80, 220);" - "}" - "QToolButton:pressed {" - " background-color: rgba(30, 30, 30, 240);" - "}"; - - m_btnZoomIn = new QToolButton(m_glWidget); - m_btnZoomIn->setText("+"); - m_btnZoomIn->setFixedSize(zoomBtnSize, zoomBtnSize); - m_btnZoomIn->setStyleSheet(zoomBtnStyle); - m_btnZoomIn->setAutoRepeat(true); - m_btnZoomIn->setAutoRepeatDelay(300); - m_btnZoomIn->setAutoRepeatInterval(50); - connect(m_btnZoomIn, &QToolButton::clicked, m_glWidget, &PointCloudGLWidget::zoomIn); - - m_btnZoomOut = new QToolButton(m_glWidget); - m_btnZoomOut->setText("-"); - m_btnZoomOut->setFixedSize(zoomBtnSize, zoomBtnSize); - m_btnZoomOut->setStyleSheet(zoomBtnStyle); - m_btnZoomOut->setAutoRepeat(true); - m_btnZoomOut->setAutoRepeatDelay(300); - m_btnZoomOut->setAutoRepeatInterval(50); - connect(m_btnZoomOut, &QToolButton::clicked, m_glWidget, &PointCloudGLWidget::zoomOut); - - // 安装事件过滤器,在 GL 控件 resize 时更新按钮位置 - m_glWidget->installEventFilter(this); - - // 连接信号 - connect(m_glWidget, &PointCloudGLWidget::pointSelected, this, &CloudViewMainWindow::onPointSelected); - connect(m_glWidget, &PointCloudGLWidget::twoPointsSelected, this, &CloudViewMainWindow::onTwoPointsSelected); - connect(m_glWidget, &PointCloudGLWidget::lineSelected, this, &CloudViewMainWindow::onLineSelected); - connect(m_glWidget, &PointCloudGLWidget::viewAnglesChanged, this, &CloudViewMainWindow::onViewAnglesChanged); - - return widget; -} - -QWidget* CloudViewMainWindow::createControlPanel() -{ - QWidget* widget = new QWidget(this); - widget->setMaximumWidth(350); - QVBoxLayout* layout = new QVBoxLayout(widget); - layout->setContentsMargins(5, 5, 5, 5); - layout->setSpacing(5); - - // 文件操作组 - layout->addWidget(createFileGroup()); - - // 创建 Tab 控件 - QTabWidget* tabWidget = new QTabWidget(widget); - tabWidget->addTab(createMeasurePage(), "选点测距"); - tabWidget->addTab(createLinePage(), "选线"); - layout->addWidget(tabWidget); - - // 点云列表组 - layout->addWidget(createCloudListGroup()); - - // 添加弹性空间 - layout->addStretch(); - - return widget; -} - -QGroupBox* CloudViewMainWindow::createFileGroup() -{ - QGroupBox* group = new QGroupBox("文件操作", this); - group->setMaximumWidth(350); - QVBoxLayout* layout = new QVBoxLayout(group); - layout->setSpacing(3); - layout->setContentsMargins(5, 5, 5, 5); - - m_btnOpenFile = new QPushButton("打开文件", group); - m_btnOpenFile->setMinimumHeight(24); - m_btnOpenFile->setMaximumHeight(24); - connect(m_btnOpenFile, &QPushButton::clicked, this, &CloudViewMainWindow::onOpenFile); - layout->addWidget(m_btnOpenFile); - - m_btnOpenBBox = new QPushButton("加载包围盒 (grasp.json)", group); - m_btnOpenBBox->setMinimumHeight(24); - m_btnOpenBBox->setMaximumHeight(24); - connect(m_btnOpenBBox, &QPushButton::clicked, this, &CloudViewMainWindow::onOpenBBoxFile); - layout->addWidget(m_btnOpenBBox); - - m_btnClearAll = new QPushButton("清除所有", group); - m_btnClearAll->setMinimumHeight(24); - m_btnClearAll->setMaximumHeight(24); - connect(m_btnClearAll, &QPushButton::clicked, this, &CloudViewMainWindow::onClearAll); - layout->addWidget(m_btnClearAll); - - return group; -} - -QWidget* CloudViewMainWindow::createViewToolbar() -{ - QWidget* widget = new QWidget(this); - QVBoxLayout* layout = new QVBoxLayout(widget); - layout->setContentsMargins(2, 2, 2, 2); - layout->setSpacing(3); - - // 视图预设:图标、提示、旋转角度 - struct ViewPreset { - const char* iconPath; - const char* tooltip; - float rotX; - float rotY; - float rotZ; - }; - - // 坐标系定义:X向右,Y向下,Z朝后 - ViewPreset presets[] = { - {":/common/resource/view_front.png", "正视 (XY面)", 180.0f, 0.0f, 0.0f}, - {":/common/resource/view_back.png", "后视", 180.0f, 180.0f, 0.0f}, - {":/common/resource/view_left.png", "左视 (YZ面)", 180.0f, 90.0f, 0.0f}, - {":/common/resource/view_right.png", "右视", 180.0f, -90.0f, 0.0f}, - {":/common/resource/view_top.png", "俯视 (XZ面)", 90.0f, 0.0f, 0.0f}, - {":/common/resource/view_bottom.png", "仰视", -90.0f, 0.0f, 0.0f}, - {":/common/resource/view_robot.png", "机械臂", -90.0f, 90.0f, 0.0f}, - }; - - int btnSize = 32; - int iconSize = 24; - - for (const auto& preset : presets) { - QToolButton* btn = new QToolButton(widget); - btn->setIcon(QIcon(preset.iconPath)); - btn->setIconSize(QSize(iconSize, iconSize)); - btn->setFixedSize(btnSize, btnSize); - btn->setToolTip(preset.tooltip); - btn->setAutoRaise(true); - - float rx = preset.rotX; - float ry = preset.rotY; - float rz = preset.rotZ; - connect(btn, &QToolButton::clicked, this, [this, rx, ry, rz]() { - m_glWidget->setViewAngles(rx, ry, rz); - m_editRotX->setText(QString::number(rx, 'f', 1)); - m_editRotY->setText(QString::number(ry, 'f', 1)); - m_editRotZ->setText(QString::number(rz, 'f', 1)); - }); - layout->addWidget(btn); - } - - layout->addStretch(); - - // 旋转角度输入(纵向紧凑布局) - m_editRotX = new QLineEdit("180.0", widget); - m_editRotY = new QLineEdit("0.0", widget); - m_editRotZ = new QLineEdit("0.0", widget); - - auto makeAngleRow = [&](const QString& label, QLineEdit* edit) { - QWidget* row = new QWidget(widget); - QVBoxLayout* rowLayout = new QVBoxLayout(row); - rowLayout->setContentsMargins(0, 0, 0, 0); - rowLayout->setSpacing(0); - QLabel* lbl = new QLabel(label, row); - lbl->setStyleSheet("font-size: 10px; color: gray;"); - rowLayout->addWidget(lbl); - edit->setFixedWidth(btnSize); - edit->setMaximumHeight(20); - edit->setStyleSheet("font-size: 10px;"); - rowLayout->addWidget(edit); - return row; - }; - - layout->addWidget(makeAngleRow("RX", m_editRotX)); - layout->addWidget(makeAngleRow("RY", m_editRotY)); - layout->addWidget(makeAngleRow("RZ", m_editRotZ)); - - // 应用按钮 - QPushButton* btnApply = new QPushButton("GO", widget); - btnApply->setFixedSize(btnSize, 20); - btnApply->setStyleSheet("font-size: 10px;"); - auto applyAngles = [this]() { - bool okX, okY, okZ; - float rotX = m_editRotX->text().toFloat(&okX); - float rotY = m_editRotY->text().toFloat(&okY); - float rotZ = m_editRotZ->text().toFloat(&okZ); - if (okX && okY && okZ) { - m_glWidget->setViewAngles(rotX, rotY, rotZ); - } - }; - connect(btnApply, &QPushButton::clicked, this, applyAngles); - connect(m_editRotX, &QLineEdit::returnPressed, this, applyAngles); - connect(m_editRotY, &QLineEdit::returnPressed, this, applyAngles); - connect(m_editRotZ, &QLineEdit::returnPressed, this, applyAngles); - layout->addWidget(btnApply); - - widget->setFixedWidth(btnSize + 8); - return widget; -} - -QWidget* CloudViewMainWindow::createMeasurePage() -{ - QWidget* page = new QWidget(this); - QVBoxLayout* layout = new QVBoxLayout(page); - layout->setContentsMargins(0, 5, 0, 0); - layout->addWidget(createMeasureGroup()); - layout->addStretch(); - return page; -} - -QWidget* CloudViewMainWindow::createLinePage() -{ - QWidget* page = new QWidget(this); - QVBoxLayout* layout = new QVBoxLayout(page); - layout->setContentsMargins(0, 5, 0, 0); - - // 选线拟合组 - layout->addWidget(createLineGroup()); - - // 输入线段组 - QGroupBox* inputLineGroup = new QGroupBox("输入线段", page); - QVBoxLayout* inputLayout = new QVBoxLayout(inputLineGroup); - - // 提示 - QLabel* lblTip = new QLabel("输入两点坐标显示线段", inputLineGroup); - lblTip->setStyleSheet("color: gray; font-size: 10px;"); - inputLayout->addWidget(lblTip); - - // 点1坐标 - QLabel* lblPoint1 = new QLabel("点1:", inputLineGroup); - lblPoint1->setStyleSheet("font-weight: bold;"); - inputLayout->addWidget(lblPoint1); - - QHBoxLayout* p1Layout = new QHBoxLayout(); - p1Layout->addWidget(new QLabel("X:", inputLineGroup)); - m_editLineX1 = new QLineEdit("0.0", inputLineGroup); - m_editLineX1->setMaximumWidth(70); - p1Layout->addWidget(m_editLineX1); - - p1Layout->addWidget(new QLabel("Y:", inputLineGroup)); - m_editLineY1 = new QLineEdit("0.0", inputLineGroup); - m_editLineY1->setMaximumWidth(70); - p1Layout->addWidget(m_editLineY1); - - p1Layout->addWidget(new QLabel("Z:", inputLineGroup)); - m_editLineZ1 = new QLineEdit("0.0", inputLineGroup); - m_editLineZ1->setMaximumWidth(70); - p1Layout->addWidget(m_editLineZ1); - inputLayout->addLayout(p1Layout); - - // 点2坐标 - QLabel* lblPoint2 = new QLabel("点2:", inputLineGroup); - lblPoint2->setStyleSheet("font-weight: bold;"); - inputLayout->addWidget(lblPoint2); - - QHBoxLayout* p2Layout = new QHBoxLayout(); - p2Layout->addWidget(new QLabel("X:", inputLineGroup)); - m_editLineX2 = new QLineEdit("100.0", inputLineGroup); - m_editLineX2->setMaximumWidth(70); - p2Layout->addWidget(m_editLineX2); - - p2Layout->addWidget(new QLabel("Y:", inputLineGroup)); - m_editLineY2 = new QLineEdit("100.0", inputLineGroup); - m_editLineY2->setMaximumWidth(70); - p2Layout->addWidget(m_editLineY2); - - p2Layout->addWidget(new QLabel("Z:", inputLineGroup)); - m_editLineZ2 = new QLineEdit("100.0", inputLineGroup); - m_editLineZ2->setMaximumWidth(70); - p2Layout->addWidget(m_editLineZ2); - inputLayout->addLayout(p2Layout); - - // 按钮 - QHBoxLayout* btnLayout = new QHBoxLayout(); - m_btnShowLine = new QPushButton("显示线段", inputLineGroup); - connect(m_btnShowLine, &QPushButton::clicked, this, &CloudViewMainWindow::onShowInputLine); - btnLayout->addWidget(m_btnShowLine); - - m_btnClearLine2 = new QPushButton("清除线段", inputLineGroup); - connect(m_btnClearLine2, &QPushButton::clicked, this, &CloudViewMainWindow::onClearInputLine); - btnLayout->addWidget(m_btnClearLine2); - inputLayout->addLayout(btnLayout); - - // 连接输入线段坐标框的回车信号 - connect(m_editLineX1, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowInputLine); - connect(m_editLineY1, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowInputLine); - connect(m_editLineZ1, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowInputLine); - connect(m_editLineX2, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowInputLine); - connect(m_editLineY2, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowInputLine); - connect(m_editLineZ2, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowInputLine); - - layout->addWidget(inputLineGroup); - layout->addStretch(); - return page; -} - -QGroupBox* CloudViewMainWindow::createMeasureGroup() -{ - QGroupBox* group = new QGroupBox("选点测距", this); - group->setMaximumWidth(350); - QVBoxLayout* layout = new QVBoxLayout(group); - layout->setSpacing(4); - layout->setContentsMargins(5, 5, 5, 5); - - // 操作说明 - QLabel* lblTip = new QLabel("Ctrl+左键点击点云选择点", group); - lblTip->setWordWrap(true); - lblTip->setStyleSheet("color: gray; font-size: 12px;"); - layout->addWidget(lblTip); - // 测距复选框 - m_cbMeasureDistance = new QCheckBox("启用测距", group); - m_cbMeasureDistance->setChecked(false); - connect(m_cbMeasureDistance, &QCheckBox::toggled, this, [this](bool checked) { - m_glWidget->setMeasureDistanceEnabled(checked); - m_glWidget->clearSelectedPoints(); - m_lblPoint1->setText("点1: --"); - m_lblPoint2->setText("点2: --"); - m_lblDistance->setText("--"); - m_editPoint1X->setText("--"); - m_editPoint1Y->setText("--"); - m_editPoint1Z->setText("--"); - m_editPoint2X->setText("--"); - m_editPoint2Y->setText("--"); - m_editPoint2Z->setText("--"); - }); - layout->addWidget(m_cbMeasureDistance); - - // 清除选点按钮 - m_btnClearPoints = new QPushButton("清除选点", group); - m_btnClearPoints->setMinimumHeight(24); - connect(m_btnClearPoints, &QPushButton::clicked, this, &CloudViewMainWindow::onClearSelectedPoints); - layout->addWidget(m_btnClearPoints); - - // 分隔线 - QFrame* line1 = new QFrame(group); - line1->setFrameShape(QFrame::HLine); - line1->setFrameShadow(QFrame::Sunken); - layout->addWidget(line1); - - // 点1信息(坐标直接显示在标题后) - m_lblPoint1 = new QLabel("点1: --", group); - m_lblPoint1->setWordWrap(true); - m_lblPoint1->setStyleSheet("font-weight: bold; font-size: 11px;"); - layout->addWidget(m_lblPoint1); - - // 点1坐标编辑(可修改) - QHBoxLayout* coord1Layout = new QHBoxLayout(); - coord1Layout->setSpacing(5); - coord1Layout->addWidget(new QLabel("X:", group)); - m_editPoint1X = new QLineEdit("--", group); - m_editPoint1X->setMaximumWidth(70); - m_editPoint1X->setMaximumHeight(24); - coord1Layout->addWidget(m_editPoint1X); - - coord1Layout->addWidget(new QLabel("Y:", group)); - m_editPoint1Y = new QLineEdit("--", group); - m_editPoint1Y->setMaximumWidth(70); - m_editPoint1Y->setMaximumHeight(24); - coord1Layout->addWidget(m_editPoint1Y); - - coord1Layout->addWidget(new QLabel("Z:", group)); - m_editPoint1Z = new QLineEdit("--", group); - m_editPoint1Z->setMaximumWidth(70); - m_editPoint1Z->setMaximumHeight(24); - coord1Layout->addWidget(m_editPoint1Z); - coord1Layout->addStretch(); - layout->addLayout(coord1Layout); - - // 连接回车信号 - connect(m_editPoint1X, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onPoint1CoordChanged); - connect(m_editPoint1Y, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onPoint1CoordChanged); - connect(m_editPoint1Z, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onPoint1CoordChanged); - - // 点1姿态输入(紧凑布局) - QHBoxLayout* pose1Layout = new QHBoxLayout(); - pose1Layout->setSpacing(5); - pose1Layout->addWidget(new QLabel("RX:", group)); - m_editRx1 = new QLineEdit("0.0", group); - m_editRx1->setMaximumWidth(50); - pose1Layout->addWidget(m_editRx1); - - pose1Layout->addWidget(new QLabel("RY:", group)); - m_editRy1 = new QLineEdit("0.0", group); - m_editRy1->setMaximumWidth(50); - pose1Layout->addWidget(m_editRy1); - - pose1Layout->addWidget(new QLabel("RZ:", group)); - m_editRz1 = new QLineEdit("0.0", group); - m_editRz1->setMaximumWidth(50); - pose1Layout->addWidget(m_editRz1); - pose1Layout->addStretch(); - layout->addLayout(pose1Layout); - - m_btnShowPose1 = new QPushButton("显示点1姿态", group); - m_btnShowPose1->setMinimumHeight(24); - connect(m_btnShowPose1, &QPushButton::clicked, this, &CloudViewMainWindow::onShowPose1); - layout->addWidget(m_btnShowPose1); - - // 连接姿态输入框的回车信号 - connect(m_editRx1, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowPose1); - connect(m_editRy1, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowPose1); - connect(m_editRz1, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowPose1); - - // 分隔线 - QFrame* line2 = new QFrame(group); - line2->setFrameShape(QFrame::HLine); - line2->setFrameShadow(QFrame::Sunken); - layout->addWidget(line2); - - // 点2信息(坐标直接显示在标题后) - m_lblPoint2 = new QLabel("点2: --", group); - m_lblPoint2->setWordWrap(true); - m_lblPoint2->setStyleSheet("font-weight: bold; font-size: 11px;"); - layout->addWidget(m_lblPoint2); - - // 点2坐标编辑(可修改) - QHBoxLayout* coord2Layout = new QHBoxLayout(); - coord2Layout->setSpacing(5); - coord2Layout->addWidget(new QLabel("X:", group)); - m_editPoint2X = new QLineEdit("--", group); - m_editPoint2X->setMaximumWidth(70); - m_editPoint2X->setMaximumHeight(24); - coord2Layout->addWidget(m_editPoint2X); - - coord2Layout->addWidget(new QLabel("Y:", group)); - m_editPoint2Y = new QLineEdit("--", group); - m_editPoint2Y->setMaximumWidth(70); - m_editPoint2Y->setMaximumHeight(24); - coord2Layout->addWidget(m_editPoint2Y); - - coord2Layout->addWidget(new QLabel("Z:", group)); - m_editPoint2Z = new QLineEdit("--", group); - m_editPoint2Z->setMaximumWidth(70); - m_editPoint2Z->setMaximumHeight(24); - coord2Layout->addWidget(m_editPoint2Z); - coord2Layout->addStretch(); - layout->addLayout(coord2Layout); - - // 连接回车信号 - connect(m_editPoint2X, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onPoint2CoordChanged); - connect(m_editPoint2Y, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onPoint2CoordChanged); - connect(m_editPoint2Z, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onPoint2CoordChanged); - - // 点2姿态输入(紧凑布局) - QHBoxLayout* pose2Layout = new QHBoxLayout(); - pose2Layout->setSpacing(5); - pose2Layout->addWidget(new QLabel("RX:", group)); - m_editRx2 = new QLineEdit("0.0", group); - m_editRx2->setMaximumWidth(50); - pose2Layout->addWidget(m_editRx2); - - pose2Layout->addWidget(new QLabel("RY:", group)); - m_editRy2 = new QLineEdit("0.0", group); - m_editRy2->setMaximumWidth(50); - pose2Layout->addWidget(m_editRy2); - - pose2Layout->addWidget(new QLabel("RZ:", group)); - m_editRz2 = new QLineEdit("0.0", group); - m_editRz2->setMaximumWidth(50); - pose2Layout->addWidget(m_editRz2); - pose2Layout->addStretch(); - layout->addLayout(pose2Layout); - - m_btnShowPose2 = new QPushButton("显示点2姿态", group); - m_btnShowPose2->setMinimumHeight(24); - connect(m_btnShowPose2, &QPushButton::clicked, this, &CloudViewMainWindow::onShowPose2); - layout->addWidget(m_btnShowPose2); - - // 连接姿态输入框的回车信号 - connect(m_editRx2, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowPose2); - connect(m_editRy2, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowPose2); - connect(m_editRz2, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowPose2); - - // 姿态坐标轴缩放输入 - QHBoxLayout* scaleLayout = new QHBoxLayout(); - scaleLayout->setSpacing(5); - scaleLayout->addWidget(new QLabel("坐标轴长度:", group)); - m_editPoseScale = new QLineEdit("10.0", group); - m_editPoseScale->setMaximumWidth(60); - scaleLayout->addWidget(m_editPoseScale); - scaleLayout->addStretch(); - layout->addLayout(scaleLayout); - - // 分隔线 - QFrame* line3 = new QFrame(group); - line3->setFrameShape(QFrame::HLine); - line3->setFrameShadow(QFrame::Sunken); - layout->addWidget(line3); - - // 欧拉角旋转顺序选择 - QLabel* lblEulerOrder = new QLabel("欧拉角旋转顺序:", group); - lblEulerOrder->setStyleSheet("font-weight: bold; font-size: 10px;"); - layout->addWidget(lblEulerOrder); - - m_comboEulerOrder = new QComboBox(group); - m_comboEulerOrder->addItem("ZYX (Yaw-Pitch-Roll)", static_cast(EulerRotationOrder::ZYX)); - m_comboEulerOrder->addItem("XYZ (Roll-Pitch-Yaw)", static_cast(EulerRotationOrder::XYZ)); - m_comboEulerOrder->addItem("ZXY (Yaw-Roll-Pitch)", static_cast(EulerRotationOrder::ZXY)); - m_comboEulerOrder->addItem("YXZ (Pitch-Roll-Yaw)", static_cast(EulerRotationOrder::YXZ)); - m_comboEulerOrder->addItem("XZY (Roll-Yaw-Pitch)", static_cast(EulerRotationOrder::XZY)); - m_comboEulerOrder->addItem("YZX (Pitch-Yaw-Roll)", static_cast(EulerRotationOrder::YZX)); - m_comboEulerOrder->setCurrentIndex(0); - m_comboEulerOrder->setMaximumHeight(24); - connect(m_comboEulerOrder, QOverload::of(&QComboBox::currentIndexChanged), - this, &CloudViewMainWindow::onEulerOrderChanged); - layout->addWidget(m_comboEulerOrder); - - // 分隔线 - QFrame* line4 = new QFrame(group); - line4->setFrameShape(QFrame::HLine); - line4->setFrameShadow(QFrame::Sunken); - layout->addWidget(line4); - - // 距离信息 - QHBoxLayout* distLayout = new QHBoxLayout(); - QLabel* lblDistTitle = new QLabel("距离:", group); - lblDistTitle->setStyleSheet("font-size: 10px;"); - m_lblDistance = new QLabel("--", group); - m_lblDistance->setStyleSheet("font-weight: bold; color: green; font-size: 20px;"); + + QMenu* transformMenu = menuBar()->addMenu("矩阵变换"); + QAction* actEyeToHand = transformMenu->addAction("眼在手外..."); + connect(actEyeToHand, &QAction::triggered, this, &CloudViewMainWindow::onEyeToHandTransform); + QAction* actEyeInHand = transformMenu->addAction("眼在手上..."); + connect(actEyeInHand, &QAction::triggered, this, &CloudViewMainWindow::onEyeInHandTransform); + // 创建中央控件 + QWidget* centralWidget = new QWidget(this); + setCentralWidget(centralWidget); + + // 创建主布局 + QHBoxLayout* mainLayout = new QHBoxLayout(centralWidget); + mainLayout->setContentsMargins(5, 5, 5, 5); + mainLayout->setSpacing(5); + + // 创建分割器 + QSplitter* splitter = new QSplitter(Qt::Horizontal, centralWidget); + + // 左侧:视图工具栏 + 点云显示区域 + QWidget* viewerContainer = new QWidget(splitter); + QHBoxLayout* viewerLayout = new QHBoxLayout(viewerContainer); + viewerLayout->setContentsMargins(0, 0, 0, 0); + viewerLayout->setSpacing(2); + + // 视图方向工具栏(纵向排列,在3D视图左侧) + QWidget* viewToolbar = createViewToolbar(); + viewerLayout->addWidget(viewToolbar); + + // 点云显示区域 + QWidget* viewerArea = createViewerArea(); + viewerLayout->addWidget(viewerArea); + + splitter->addWidget(viewerContainer); + + // 右侧:控制面板 + QWidget* controlPanel = createControlPanel(); + splitter->addWidget(controlPanel); + + // 设置分割器初始大小(左侧 70%,右侧 30%) + splitter->setSizes({700, 300}); + + mainLayout->addWidget(splitter); + + // 状态栏 + statusBar()->showMessage("就绪"); +} + +QWidget* CloudViewMainWindow::createViewerArea() +{ + QWidget* widget = new QWidget(this); + QVBoxLayout* layout = new QVBoxLayout(widget); + layout->setContentsMargins(0, 0, 0, 0); + + m_glWidget = new PointCloudGLWidget(widget); + m_glWidget->setMinimumSize(400, 300); + m_glWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + layout->addWidget(m_glWidget); + + // 悬浮缩放按钮(父控件为 m_glWidget,悬浮在右侧中间) + int zoomBtnSize = 28; + QString zoomBtnStyle = + "QToolButton {" + " background-color: rgba(50, 50, 50, 180);" + " color: white;" + " border: 1px solid rgba(120, 120, 120, 150);" + " border-radius: 4px;" + " font-size: 32px;" + " font-weight: bold;" + "}" + "QToolButton:hover {" + " background-color: rgba(80, 80, 80, 220);" + "}" + "QToolButton:pressed {" + " background-color: rgba(30, 30, 30, 240);" + "}"; + + m_btnZoomIn = new QToolButton(m_glWidget); + m_btnZoomIn->setText("+"); + m_btnZoomIn->setFixedSize(zoomBtnSize, zoomBtnSize); + m_btnZoomIn->setStyleSheet(zoomBtnStyle); + m_btnZoomIn->setAutoRepeat(true); + m_btnZoomIn->setAutoRepeatDelay(300); + m_btnZoomIn->setAutoRepeatInterval(50); + connect(m_btnZoomIn, &QToolButton::clicked, m_glWidget, &PointCloudGLWidget::zoomIn); + + m_btnZoomOut = new QToolButton(m_glWidget); + m_btnZoomOut->setText("-"); + m_btnZoomOut->setFixedSize(zoomBtnSize, zoomBtnSize); + m_btnZoomOut->setStyleSheet(zoomBtnStyle); + m_btnZoomOut->setAutoRepeat(true); + m_btnZoomOut->setAutoRepeatDelay(300); + m_btnZoomOut->setAutoRepeatInterval(50); + connect(m_btnZoomOut, &QToolButton::clicked, m_glWidget, &PointCloudGLWidget::zoomOut); + + // 安装事件过滤器,在 GL 控件 resize 时更新按钮位置 + m_glWidget->installEventFilter(this); + + // 连接信号 + connect(m_glWidget, &PointCloudGLWidget::pointSelected, this, &CloudViewMainWindow::onPointSelected); + connect(m_glWidget, &PointCloudGLWidget::twoPointsSelected, this, &CloudViewMainWindow::onTwoPointsSelected); + connect(m_glWidget, &PointCloudGLWidget::lineSelected, this, &CloudViewMainWindow::onLineSelected); + connect(m_glWidget, &PointCloudGLWidget::viewAnglesChanged, this, &CloudViewMainWindow::onViewAnglesChanged); + + return widget; +} + +QWidget* CloudViewMainWindow::createControlPanel() +{ + QWidget* widget = new QWidget(this); + widget->setMaximumWidth(350); + QVBoxLayout* layout = new QVBoxLayout(widget); + layout->setContentsMargins(5, 5, 5, 5); + layout->setSpacing(5); + + // 文件操作组 + layout->addWidget(createFileGroup()); + + // 创建 Tab 控件 + QTabWidget* tabWidget = new QTabWidget(widget); + tabWidget->addTab(createMeasurePage(), "选点测距"); + tabWidget->addTab(createLinePage(), "选线"); + layout->addWidget(tabWidget); + + // 点云列表组 + layout->addWidget(createCloudListGroup()); + + // 添加弹性空间 + layout->addStretch(); + + return widget; +} + +QGroupBox* CloudViewMainWindow::createFileGroup() +{ + QGroupBox* group = new QGroupBox("文件操作", this); + group->setMaximumWidth(350); + QVBoxLayout* layout = new QVBoxLayout(group); + layout->setSpacing(3); + layout->setContentsMargins(5, 5, 5, 5); + + m_btnOpenFile = new QPushButton("打开文件", group); + m_btnOpenFile->setMinimumHeight(24); + m_btnOpenFile->setMaximumHeight(24); + connect(m_btnOpenFile, &QPushButton::clicked, this, &CloudViewMainWindow::onOpenFile); + layout->addWidget(m_btnOpenFile); + + m_btnOpenBBox = new QPushButton("加载包围盒 (grasp.json)", group); + m_btnOpenBBox->setMinimumHeight(24); + m_btnOpenBBox->setMaximumHeight(24); + connect(m_btnOpenBBox, &QPushButton::clicked, this, &CloudViewMainWindow::onOpenBBoxFile); + layout->addWidget(m_btnOpenBBox); + + m_btnClearAll = new QPushButton("清除所有", group); + m_btnClearAll->setMinimumHeight(24); + m_btnClearAll->setMaximumHeight(24); + connect(m_btnClearAll, &QPushButton::clicked, this, &CloudViewMainWindow::onClearAll); + layout->addWidget(m_btnClearAll); + + return group; +} + +QWidget* CloudViewMainWindow::createViewToolbar() +{ + QWidget* widget = new QWidget(this); + QVBoxLayout* layout = new QVBoxLayout(widget); + layout->setContentsMargins(2, 2, 2, 2); + layout->setSpacing(3); + + // 视图预设:图标、提示、旋转角度 + struct ViewPreset { + const char* iconPath; + const char* tooltip; + float rotX; + float rotY; + float rotZ; + }; + + // 坐标系定义:X向右,Y向下,Z朝后 + ViewPreset presets[] = { + {":/common/resource/view_front.png", "正视 (XY面)", 180.0f, 0.0f, 0.0f}, + {":/common/resource/view_back.png", "后视", 180.0f, 180.0f, 0.0f}, + {":/common/resource/view_left.png", "左视 (YZ面)", 180.0f, 90.0f, 0.0f}, + {":/common/resource/view_right.png", "右视", 180.0f, -90.0f, 0.0f}, + {":/common/resource/view_top.png", "俯视 (XZ面)", 90.0f, 0.0f, 0.0f}, + {":/common/resource/view_bottom.png", "仰视", -90.0f, 0.0f, 0.0f}, + {":/common/resource/view_robot.png", "机械臂", -90.0f, 90.0f, 0.0f}, + }; + + int btnSize = 32; + int iconSize = 24; + + for (const auto& preset : presets) { + QToolButton* btn = new QToolButton(widget); + btn->setIcon(QIcon(preset.iconPath)); + btn->setIconSize(QSize(iconSize, iconSize)); + btn->setFixedSize(btnSize, btnSize); + btn->setToolTip(preset.tooltip); + btn->setAutoRaise(true); + + float rx = preset.rotX; + float ry = preset.rotY; + float rz = preset.rotZ; + connect(btn, &QToolButton::clicked, this, [this, rx, ry, rz]() { + m_glWidget->setViewAngles(rx, ry, rz); + m_editRotX->setText(QString::number(rx, 'f', 1)); + m_editRotY->setText(QString::number(ry, 'f', 1)); + m_editRotZ->setText(QString::number(rz, 'f', 1)); + }); + layout->addWidget(btn); + } + + layout->addStretch(); + + // 旋转角度输入(纵向紧凑布局) + m_editRotX = new QLineEdit("180.0", widget); + m_editRotY = new QLineEdit("0.0", widget); + m_editRotZ = new QLineEdit("0.0", widget); + + auto makeAngleRow = [&](const QString& label, QLineEdit* edit) { + QWidget* row = new QWidget(widget); + QVBoxLayout* rowLayout = new QVBoxLayout(row); + rowLayout->setContentsMargins(0, 0, 0, 0); + rowLayout->setSpacing(0); + QLabel* lbl = new QLabel(label, row); + lbl->setStyleSheet("font-size: 10px; color: gray;"); + rowLayout->addWidget(lbl); + edit->setFixedWidth(btnSize); + edit->setMaximumHeight(20); + edit->setStyleSheet("font-size: 10px;"); + rowLayout->addWidget(edit); + return row; + }; + + layout->addWidget(makeAngleRow("RX", m_editRotX)); + layout->addWidget(makeAngleRow("RY", m_editRotY)); + layout->addWidget(makeAngleRow("RZ", m_editRotZ)); + + // 应用按钮 + QPushButton* btnApply = new QPushButton("GO", widget); + btnApply->setFixedSize(btnSize, 20); + btnApply->setStyleSheet("font-size: 10px;"); + auto applyAngles = [this]() { + bool okX, okY, okZ; + float rotX = m_editRotX->text().toFloat(&okX); + float rotY = m_editRotY->text().toFloat(&okY); + float rotZ = m_editRotZ->text().toFloat(&okZ); + if (okX && okY && okZ) { + m_glWidget->setViewAngles(rotX, rotY, rotZ); + } + }; + connect(btnApply, &QPushButton::clicked, this, applyAngles); + connect(m_editRotX, &QLineEdit::returnPressed, this, applyAngles); + connect(m_editRotY, &QLineEdit::returnPressed, this, applyAngles); + connect(m_editRotZ, &QLineEdit::returnPressed, this, applyAngles); + layout->addWidget(btnApply); + + widget->setFixedWidth(btnSize + 8); + return widget; +} + +QWidget* CloudViewMainWindow::createMeasurePage() +{ + QWidget* page = new QWidget(this); + QVBoxLayout* layout = new QVBoxLayout(page); + layout->setContentsMargins(0, 5, 0, 0); + layout->addWidget(createMeasureGroup()); + layout->addStretch(); + return page; +} + +QWidget* CloudViewMainWindow::createLinePage() +{ + QWidget* page = new QWidget(this); + QVBoxLayout* layout = new QVBoxLayout(page); + layout->setContentsMargins(0, 5, 0, 0); + + // 选线拟合组 + layout->addWidget(createLineGroup()); + + // 输入线段组 + QGroupBox* inputLineGroup = new QGroupBox("输入线段", page); + QVBoxLayout* inputLayout = new QVBoxLayout(inputLineGroup); + + // 提示 + QLabel* lblTip = new QLabel("输入两点坐标显示线段", inputLineGroup); + lblTip->setStyleSheet("color: gray; font-size: 10px;"); + inputLayout->addWidget(lblTip); + + // 点1坐标 + QLabel* lblPoint1 = new QLabel("点1:", inputLineGroup); + lblPoint1->setStyleSheet("font-weight: bold;"); + inputLayout->addWidget(lblPoint1); + + QHBoxLayout* p1Layout = new QHBoxLayout(); + p1Layout->addWidget(new QLabel("X:", inputLineGroup)); + m_editLineX1 = new QLineEdit("0.0", inputLineGroup); + m_editLineX1->setMaximumWidth(70); + p1Layout->addWidget(m_editLineX1); + + p1Layout->addWidget(new QLabel("Y:", inputLineGroup)); + m_editLineY1 = new QLineEdit("0.0", inputLineGroup); + m_editLineY1->setMaximumWidth(70); + p1Layout->addWidget(m_editLineY1); + + p1Layout->addWidget(new QLabel("Z:", inputLineGroup)); + m_editLineZ1 = new QLineEdit("0.0", inputLineGroup); + m_editLineZ1->setMaximumWidth(70); + p1Layout->addWidget(m_editLineZ1); + inputLayout->addLayout(p1Layout); + + // 点2坐标 + QLabel* lblPoint2 = new QLabel("点2:", inputLineGroup); + lblPoint2->setStyleSheet("font-weight: bold;"); + inputLayout->addWidget(lblPoint2); + + QHBoxLayout* p2Layout = new QHBoxLayout(); + p2Layout->addWidget(new QLabel("X:", inputLineGroup)); + m_editLineX2 = new QLineEdit("100.0", inputLineGroup); + m_editLineX2->setMaximumWidth(70); + p2Layout->addWidget(m_editLineX2); + + p2Layout->addWidget(new QLabel("Y:", inputLineGroup)); + m_editLineY2 = new QLineEdit("100.0", inputLineGroup); + m_editLineY2->setMaximumWidth(70); + p2Layout->addWidget(m_editLineY2); + + p2Layout->addWidget(new QLabel("Z:", inputLineGroup)); + m_editLineZ2 = new QLineEdit("100.0", inputLineGroup); + m_editLineZ2->setMaximumWidth(70); + p2Layout->addWidget(m_editLineZ2); + inputLayout->addLayout(p2Layout); + + // 按钮 + QHBoxLayout* btnLayout = new QHBoxLayout(); + m_btnShowLine = new QPushButton("显示线段", inputLineGroup); + connect(m_btnShowLine, &QPushButton::clicked, this, &CloudViewMainWindow::onShowInputLine); + btnLayout->addWidget(m_btnShowLine); + + m_btnClearLine2 = new QPushButton("清除线段", inputLineGroup); + connect(m_btnClearLine2, &QPushButton::clicked, this, &CloudViewMainWindow::onClearInputLine); + btnLayout->addWidget(m_btnClearLine2); + inputLayout->addLayout(btnLayout); + + // 连接输入线段坐标框的回车信号 + connect(m_editLineX1, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowInputLine); + connect(m_editLineY1, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowInputLine); + connect(m_editLineZ1, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowInputLine); + connect(m_editLineX2, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowInputLine); + connect(m_editLineY2, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowInputLine); + connect(m_editLineZ2, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowInputLine); + + layout->addWidget(inputLineGroup); + layout->addStretch(); + return page; +} + +QGroupBox* CloudViewMainWindow::createMeasureGroup() +{ + QGroupBox* group = new QGroupBox("选点测距", this); + group->setMaximumWidth(350); + QVBoxLayout* layout = new QVBoxLayout(group); + layout->setSpacing(4); + layout->setContentsMargins(5, 5, 5, 5); + + // 操作说明 + QLabel* lblTip = new QLabel("Ctrl+左键点击点云选择点", group); + lblTip->setWordWrap(true); + lblTip->setStyleSheet("color: gray; font-size: 12px;"); + layout->addWidget(lblTip); + // 测距复选框 + m_cbMeasureDistance = new QCheckBox("启用测距", group); + m_cbMeasureDistance->setChecked(false); + connect(m_cbMeasureDistance, &QCheckBox::toggled, this, [this](bool checked) { + m_glWidget->setMeasureDistanceEnabled(checked); + m_glWidget->clearSelectedPoints(); + m_lblPoint1->setText("点1: --"); + m_lblPoint2->setText("点2: --"); + clearMeasureResultDisplay(); + m_editPoint1X->setText("--"); + m_editPoint1Y->setText("--"); + m_editPoint1Z->setText("--"); + m_editPoint2X->setText("--"); + m_editPoint2Y->setText("--"); + m_editPoint2Z->setText("--"); + }); + layout->addWidget(m_cbMeasureDistance); + + // 清除选点按钮 + m_btnClearPoints = new QPushButton("清除选点", group); + m_btnClearPoints->setMinimumHeight(24); + connect(m_btnClearPoints, &QPushButton::clicked, this, &CloudViewMainWindow::onClearSelectedPoints); + layout->addWidget(m_btnClearPoints); + + // 分隔线 + QFrame* line1 = new QFrame(group); + line1->setFrameShape(QFrame::HLine); + line1->setFrameShadow(QFrame::Sunken); + layout->addWidget(line1); + + // 点1信息(坐标直接显示在标题后) + m_lblPoint1 = new QLabel("点1: --", group); + m_lblPoint1->setWordWrap(true); + m_lblPoint1->setStyleSheet("font-weight: bold; font-size: 11px;"); + layout->addWidget(m_lblPoint1); + + // 点1坐标编辑(可修改) + QHBoxLayout* coord1Layout = new QHBoxLayout(); + coord1Layout->setSpacing(5); + coord1Layout->addWidget(new QLabel("X:", group)); + m_editPoint1X = new QLineEdit("--", group); + m_editPoint1X->setMaximumWidth(70); + m_editPoint1X->setMaximumHeight(24); + coord1Layout->addWidget(m_editPoint1X); + + coord1Layout->addWidget(new QLabel("Y:", group)); + m_editPoint1Y = new QLineEdit("--", group); + m_editPoint1Y->setMaximumWidth(70); + m_editPoint1Y->setMaximumHeight(24); + coord1Layout->addWidget(m_editPoint1Y); + + coord1Layout->addWidget(new QLabel("Z:", group)); + m_editPoint1Z = new QLineEdit("--", group); + m_editPoint1Z->setMaximumWidth(70); + m_editPoint1Z->setMaximumHeight(24); + coord1Layout->addWidget(m_editPoint1Z); + coord1Layout->addStretch(); + layout->addLayout(coord1Layout); + + // 连接回车信号 + connect(m_editPoint1X, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onPoint1CoordChanged); + connect(m_editPoint1Y, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onPoint1CoordChanged); + connect(m_editPoint1Z, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onPoint1CoordChanged); + + // 点1姿态输入(紧凑布局) + QHBoxLayout* pose1Layout = new QHBoxLayout(); + pose1Layout->setSpacing(5); + pose1Layout->addWidget(new QLabel("RX:", group)); + m_editRx1 = new QLineEdit("0.0", group); + m_editRx1->setMaximumWidth(50); + pose1Layout->addWidget(m_editRx1); + + pose1Layout->addWidget(new QLabel("RY:", group)); + m_editRy1 = new QLineEdit("0.0", group); + m_editRy1->setMaximumWidth(50); + pose1Layout->addWidget(m_editRy1); + + pose1Layout->addWidget(new QLabel("RZ:", group)); + m_editRz1 = new QLineEdit("0.0", group); + m_editRz1->setMaximumWidth(50); + pose1Layout->addWidget(m_editRz1); + pose1Layout->addStretch(); + layout->addLayout(pose1Layout); + + m_btnShowPose1 = new QPushButton("显示点1姿态", group); + m_btnShowPose1->setMinimumHeight(24); + connect(m_btnShowPose1, &QPushButton::clicked, this, &CloudViewMainWindow::onShowPose1); + layout->addWidget(m_btnShowPose1); + + // 连接姿态输入框的回车信号 + connect(m_editRx1, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowPose1); + connect(m_editRy1, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowPose1); + connect(m_editRz1, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowPose1); + + // 分隔线 + QFrame* line2 = new QFrame(group); + line2->setFrameShape(QFrame::HLine); + line2->setFrameShadow(QFrame::Sunken); + layout->addWidget(line2); + + // 点2信息(坐标直接显示在标题后) + m_lblPoint2 = new QLabel("点2: --", group); + m_lblPoint2->setWordWrap(true); + m_lblPoint2->setStyleSheet("font-weight: bold; font-size: 11px;"); + layout->addWidget(m_lblPoint2); + + // 点2坐标编辑(可修改) + QHBoxLayout* coord2Layout = new QHBoxLayout(); + coord2Layout->setSpacing(5); + coord2Layout->addWidget(new QLabel("X:", group)); + m_editPoint2X = new QLineEdit("--", group); + m_editPoint2X->setMaximumWidth(70); + m_editPoint2X->setMaximumHeight(24); + coord2Layout->addWidget(m_editPoint2X); + + coord2Layout->addWidget(new QLabel("Y:", group)); + m_editPoint2Y = new QLineEdit("--", group); + m_editPoint2Y->setMaximumWidth(70); + m_editPoint2Y->setMaximumHeight(24); + coord2Layout->addWidget(m_editPoint2Y); + + coord2Layout->addWidget(new QLabel("Z:", group)); + m_editPoint2Z = new QLineEdit("--", group); + m_editPoint2Z->setMaximumWidth(70); + m_editPoint2Z->setMaximumHeight(24); + coord2Layout->addWidget(m_editPoint2Z); + coord2Layout->addStretch(); + layout->addLayout(coord2Layout); + + // 连接回车信号 + connect(m_editPoint2X, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onPoint2CoordChanged); + connect(m_editPoint2Y, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onPoint2CoordChanged); + connect(m_editPoint2Z, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onPoint2CoordChanged); + + // 点2姿态输入(紧凑布局) + QHBoxLayout* pose2Layout = new QHBoxLayout(); + pose2Layout->setSpacing(5); + pose2Layout->addWidget(new QLabel("RX:", group)); + m_editRx2 = new QLineEdit("0.0", group); + m_editRx2->setMaximumWidth(50); + pose2Layout->addWidget(m_editRx2); + + pose2Layout->addWidget(new QLabel("RY:", group)); + m_editRy2 = new QLineEdit("0.0", group); + m_editRy2->setMaximumWidth(50); + pose2Layout->addWidget(m_editRy2); + + pose2Layout->addWidget(new QLabel("RZ:", group)); + m_editRz2 = new QLineEdit("0.0", group); + m_editRz2->setMaximumWidth(50); + pose2Layout->addWidget(m_editRz2); + pose2Layout->addStretch(); + layout->addLayout(pose2Layout); + + m_btnShowPose2 = new QPushButton("显示点2姿态", group); + m_btnShowPose2->setMinimumHeight(24); + connect(m_btnShowPose2, &QPushButton::clicked, this, &CloudViewMainWindow::onShowPose2); + layout->addWidget(m_btnShowPose2); + + // 连接姿态输入框的回车信号 + connect(m_editRx2, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowPose2); + connect(m_editRy2, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowPose2); + connect(m_editRz2, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowPose2); + + // 姿态坐标轴缩放输入 + QHBoxLayout* scaleLayout = new QHBoxLayout(); + scaleLayout->setSpacing(5); + scaleLayout->addWidget(new QLabel("坐标轴长度:", group)); + m_editPoseScale = new QLineEdit("10.0", group); + m_editPoseScale->setMaximumWidth(60); + scaleLayout->addWidget(m_editPoseScale); + scaleLayout->addStretch(); + layout->addLayout(scaleLayout); + + // 分隔线 + QFrame* line3 = new QFrame(group); + line3->setFrameShape(QFrame::HLine); + line3->setFrameShadow(QFrame::Sunken); + layout->addWidget(line3); + + // 欧拉角旋转顺序选择 + QLabel* lblEulerOrder = new QLabel("欧拉角旋转顺序:", group); + lblEulerOrder->setStyleSheet("font-weight: bold; font-size: 10px;"); + layout->addWidget(lblEulerOrder); + + m_comboEulerOrder = new QComboBox(group); + m_comboEulerOrder->addItem("ZYX (Yaw-Pitch-Roll)", static_cast(EulerRotationOrder::ZYX)); + m_comboEulerOrder->addItem("XYZ (Roll-Pitch-Yaw)", static_cast(EulerRotationOrder::XYZ)); + m_comboEulerOrder->addItem("ZXY (Yaw-Roll-Pitch)", static_cast(EulerRotationOrder::ZXY)); + m_comboEulerOrder->addItem("YXZ (Pitch-Roll-Yaw)", static_cast(EulerRotationOrder::YXZ)); + m_comboEulerOrder->addItem("XZY (Roll-Yaw-Pitch)", static_cast(EulerRotationOrder::XZY)); + m_comboEulerOrder->addItem("YZX (Pitch-Yaw-Roll)", static_cast(EulerRotationOrder::YZX)); + m_comboEulerOrder->setCurrentIndex(0); + m_comboEulerOrder->setMaximumHeight(24); + connect(m_comboEulerOrder, QOverload::of(&QComboBox::currentIndexChanged), + this, &CloudViewMainWindow::onEulerOrderChanged); + layout->addWidget(m_comboEulerOrder); + + // 分隔线 + QFrame* line4 = new QFrame(group); + line4->setFrameShape(QFrame::HLine); + line4->setFrameShadow(QFrame::Sunken); + layout->addWidget(line4); + + // 距离信息 + QHBoxLayout* distLayout = new QHBoxLayout(); + QLabel* lblDistTitle = new QLabel("距离:", group); + lblDistTitle->setStyleSheet("font-size: 10px;"); + m_lblDistance = new QLabel("--", group); + m_lblDistance->setStyleSheet("font-weight: bold; color: green; font-size: 20px;"); distLayout->addWidget(lblDistTitle); distLayout->addWidget(m_lblDistance, 1); layout->addLayout(distLayout); - return group; -} - -QGroupBox* CloudViewMainWindow::createLineGroup() -{ - QGroupBox* group = new QGroupBox("选线", this); - group->setMaximumWidth(400); - QVBoxLayout* layout = new QVBoxLayout(group); - layout->setSpacing(3); - layout->setContentsMargins(5, 5, 5, 5); - - // 操作说明 - QLabel* lblTip = new QLabel("Shift+左键点击点云选择线", group); - lblTip->setWordWrap(true); - lblTip->setStyleSheet("color: gray; font-size: 9px;"); - layout->addWidget(lblTip); - - // 选线模式选择 - QHBoxLayout* modeLayout = new QHBoxLayout(); - modeLayout->setSpacing(5); - m_rbVertical = new QRadioButton("纵向", group); - m_rbHorizontal = new QRadioButton("横向", group); - m_rbVertical->setChecked(true); - connect(m_rbVertical, &QRadioButton::toggled, this, &CloudViewMainWindow::onLineSelectModeChanged); - modeLayout->addWidget(m_rbVertical); - modeLayout->addWidget(m_rbHorizontal); - modeLayout->addStretch(); - layout->addLayout(modeLayout); - - // 输入索引选择 - QHBoxLayout* inputLayout = new QHBoxLayout(); - inputLayout->setSpacing(5); - m_lineNumberInput = new QLineEdit(group); - m_lineNumberInput->setPlaceholderText("输入索引"); - m_lineNumberInput->setMaximumHeight(24); - m_btnSelectByNumber = new QPushButton("选择", group); - m_btnSelectByNumber->setMaximumHeight(24); - m_btnSelectByNumber->setMaximumWidth(50); - connect(m_btnSelectByNumber, &QPushButton::clicked, this, &CloudViewMainWindow::onSelectLineByNumber); - connect(m_lineNumberInput, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onSelectLineByNumber); - inputLayout->addWidget(m_lineNumberInput, 1); - inputLayout->addWidget(m_btnSelectByNumber); - layout->addLayout(inputLayout); - - // 清除选线按钮 - m_btnClearLine = new QPushButton("清除选线", group); - m_btnClearLine->setMinimumHeight(24); - connect(m_btnClearLine, &QPushButton::clicked, this, &CloudViewMainWindow::onClearLinePoints); - layout->addWidget(m_btnClearLine); - - // 显示线上点按钮 - m_btnShowLinePoints = new QPushButton("显示线上点", group); - m_btnShowLinePoints->setMinimumHeight(24); - connect(m_btnShowLinePoints, &QPushButton::clicked, this, &CloudViewMainWindow::onShowLinePoints); - layout->addWidget(m_btnShowLinePoints); - - // 线索引信息 - QHBoxLayout* indexLayout = new QHBoxLayout(); - QLabel* lblIndexTitle = new QLabel("索引:", group); - lblIndexTitle->setStyleSheet("font-size: 10px;"); - m_lblLineIndex = new QLabel("--", group); - m_lblLineIndex->setStyleSheet("font-size: 10px;"); - indexLayout->addWidget(lblIndexTitle); - indexLayout->addWidget(m_lblLineIndex, 1); - layout->addLayout(indexLayout); - - // 点数信息 - QHBoxLayout* countLayout = new QHBoxLayout(); - QLabel* lblCountTitle = new QLabel("点数:", group); - lblCountTitle->setStyleSheet("font-size: 10px;"); - m_lblLinePointCount = new QLabel("--", group); - m_lblLinePointCount->setStyleSheet("font-size: 10px;"); - countLayout->addWidget(lblCountTitle); - countLayout->addWidget(m_lblLinePointCount, 1); - layout->addLayout(countLayout); + QHBoxLayout* deltaLayout = new QHBoxLayout(); + QLabel* lblDeltaTitle = new QLabel("差值:", group); + lblDeltaTitle->setStyleSheet("font-size: 10px;"); + m_lblDeltaXYZ = new QLabel("--", group); + m_lblDeltaXYZ->setStyleSheet("font-weight: bold; color: #1f6feb; font-size: 13px;"); + deltaLayout->addWidget(lblDeltaTitle); + deltaLayout->addWidget(m_lblDeltaXYZ, 1); + layout->addLayout(deltaLayout); return group; } - -QGroupBox* CloudViewMainWindow::createCloudListGroup() -{ - QGroupBox* group = new QGroupBox("点云列表", this); - group->setMaximumWidth(350); - QVBoxLayout* layout = new QVBoxLayout(group); - layout->setSpacing(3); - layout->setContentsMargins(5, 5, 5, 5); - - m_cloudList = new QListWidget(group); - m_cloudList->setMinimumHeight(70); - layout->addWidget(m_cloudList); - - return group; -} - -void CloudViewMainWindow::onOpenFile() -{ - // 如果已有打开的文件,提示是否清除 - if (m_cloudList->count() > 0) { - auto ret = QMessageBox::question(this, "提示", - "当前已有打开的文件,是否清除?", - QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); - if (ret == QMessageBox::Cancel) { - return; - } - if (ret == QMessageBox::Yes) { - onClearAll(); - } - } - - QString fileName = QFileDialog::getOpenFileName( - this, - "打开文件", - QString(), - "所有支持格式 (*.pcd *.ply *.txt *.dat);;PCD 文件 (*.pcd);;PLY 文件 (*.ply);;TXT 文件 (*.txt);;DAT 文件 (*.dat);;所有文件 (*.*)" - ); - - if (fileName.isEmpty()) { - return; - } - + +QGroupBox* CloudViewMainWindow::createLineGroup() +{ + QGroupBox* group = new QGroupBox("选线", this); + group->setMaximumWidth(400); + QVBoxLayout* layout = new QVBoxLayout(group); + layout->setSpacing(3); + layout->setContentsMargins(5, 5, 5, 5); + + // 操作说明 + QLabel* lblTip = new QLabel("Shift+左键点击点云选择线", group); + lblTip->setWordWrap(true); + lblTip->setStyleSheet("color: gray; font-size: 9px;"); + layout->addWidget(lblTip); + + // 选线模式选择 + QHBoxLayout* modeLayout = new QHBoxLayout(); + modeLayout->setSpacing(5); + m_rbVertical = new QRadioButton("纵向", group); + m_rbHorizontal = new QRadioButton("横向", group); + m_rbVertical->setChecked(true); + connect(m_rbVertical, &QRadioButton::toggled, this, &CloudViewMainWindow::onLineSelectModeChanged); + modeLayout->addWidget(m_rbVertical); + modeLayout->addWidget(m_rbHorizontal); + modeLayout->addStretch(); + layout->addLayout(modeLayout); + + // 输入索引选择 + QHBoxLayout* inputLayout = new QHBoxLayout(); + inputLayout->setSpacing(5); + m_lineNumberInput = new QLineEdit(group); + m_lineNumberInput->setPlaceholderText("输入索引"); + m_lineNumberInput->setMaximumHeight(24); + m_btnSelectByNumber = new QPushButton("选择", group); + m_btnSelectByNumber->setMaximumHeight(24); + m_btnSelectByNumber->setMaximumWidth(50); + connect(m_btnSelectByNumber, &QPushButton::clicked, this, &CloudViewMainWindow::onSelectLineByNumber); + connect(m_lineNumberInput, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onSelectLineByNumber); + inputLayout->addWidget(m_lineNumberInput, 1); + inputLayout->addWidget(m_btnSelectByNumber); + layout->addLayout(inputLayout); + + // 清除选线按钮 + m_btnClearLine = new QPushButton("清除选线", group); + m_btnClearLine->setMinimumHeight(24); + connect(m_btnClearLine, &QPushButton::clicked, this, &CloudViewMainWindow::onClearLinePoints); + layout->addWidget(m_btnClearLine); + + // 显示线上点按钮 + m_btnShowLinePoints = new QPushButton("显示线上点", group); + m_btnShowLinePoints->setMinimumHeight(24); + connect(m_btnShowLinePoints, &QPushButton::clicked, this, &CloudViewMainWindow::onShowLinePoints); + layout->addWidget(m_btnShowLinePoints); + + // 线索引信息 + QHBoxLayout* indexLayout = new QHBoxLayout(); + QLabel* lblIndexTitle = new QLabel("索引:", group); + lblIndexTitle->setStyleSheet("font-size: 10px;"); + m_lblLineIndex = new QLabel("--", group); + m_lblLineIndex->setStyleSheet("font-size: 10px;"); + indexLayout->addWidget(lblIndexTitle); + indexLayout->addWidget(m_lblLineIndex, 1); + layout->addLayout(indexLayout); + + // 点数信息 + QHBoxLayout* countLayout = new QHBoxLayout(); + QLabel* lblCountTitle = new QLabel("点数:", group); + lblCountTitle->setStyleSheet("font-size: 10px;"); + m_lblLinePointCount = new QLabel("--", group); + m_lblLinePointCount->setStyleSheet("font-size: 10px;"); + countLayout->addWidget(lblCountTitle); + countLayout->addWidget(m_lblLinePointCount, 1); + layout->addLayout(countLayout); + + return group; +} + +QGroupBox* CloudViewMainWindow::createCloudListGroup() +{ + QGroupBox* group = new QGroupBox("点云列表", this); + group->setMaximumWidth(350); + QVBoxLayout* layout = new QVBoxLayout(group); + layout->setSpacing(3); + layout->setContentsMargins(5, 5, 5, 5); + + m_cloudList = new QListWidget(group); + m_cloudList->setMinimumHeight(70); + layout->addWidget(m_cloudList); + + return group; +} + +void CloudViewMainWindow::onOpenFile() +{ + // 如果已有打开的文件,提示是否清除 + if (m_cloudList->count() > 0) { + auto ret = QMessageBox::question(this, "提示", + "当前已有打开的文件,是否清除?", + QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); + if (ret == QMessageBox::Cancel) { + return; + } + if (ret == QMessageBox::Yes) { + onClearAll(); + } + } + + QString fileName = QFileDialog::getOpenFileName( + this, + "打开文件", + QString(), + "所有支持格式 (*.pcd *.ply *.txt *.dat);;PCD 文件 (*.pcd);;PLY 文件 (*.ply);;TXT 文件 (*.txt);;DAT 文件 (*.dat);;所有文件 (*.*)" + ); + + if (fileName.isEmpty()) { + return; + } + if (!loadDisplayFile(fileName)) { QMessageBox::critical(this, "错误", "无法从文件中加载点云或线段数据"); statusBar()->showMessage("加载失败"); } } - -bool CloudViewMainWindow::loadPointCloudFile(const QString& fileName) + +bool CloudViewMainWindow::loadPointCloudFile(const QString& fileName) +{ + statusBar()->showMessage("正在加载点云..."); + + QFileInfo fileInfo(fileName); + QString ext = fileInfo.suffix().toLower(); + QString cloudName = QString("Cloud_%1 (%2)").arg(++m_cloudCount).arg(fileInfo.fileName()); + + // 统一使用 PointCloudXYZRGB 加载,支持带颜色和不带颜色的文件 + PointCloudXYZRGB rgbCloud; + int result = m_converter->loadFromFile(fileName.toStdString(), rgbCloud); + + if (result != 0) { + LOG_INFO("[CloudView] Load point cloud failed: %s\n", m_converter->getLastError().c_str()); + return false; + } + + // 保存原始完整点云 XYZ(用于旋转/线上点等功能) + m_originalCloud.clear(); + m_originalCloud.reserve(rgbCloud.size()); + for (size_t i = 0; i < rgbCloud.points.size(); ++i) { + const auto& pt = rgbCloud.points[i]; + Point3D xyzPt(pt.x, pt.y, pt.z); + int lineIdx = (i < rgbCloud.lineIndices.size()) ? rgbCloud.lineIndices[i] : 0; + m_originalCloud.push_back(xyzPt, lineIdx); + } + + // 根据是否有颜色选择显示方式 + bool hadColor = m_converter->lastLoadHadColor(); + + if (ext == "pcd" || ext == "ply") { + // PCD/PLY 文件始终走 XYZRGB 路径,避免被颜色轮换表染成蓝色等 + if (!hadColor) { + // 无 rgb 字段:默认灰色显示 + for (size_t i = 0; i < rgbCloud.points.size(); ++i) { + rgbCloud.points[i].r = 192; + rgbCloud.points[i].g = 192; + rgbCloud.points[i].b = 192; + } + } + m_glWidget->addPointCloud(rgbCloud, cloudName); + LOG_INFO("[CloudView] PCD/PLY loaded with XYZRGB path (hasRgbField=%d), points: %zu\n", + m_converter->lastLoadHadColor(), rgbCloud.size()); + } else if (hadColor) { + m_glWidget->addPointCloud(rgbCloud, cloudName); + LOG_INFO("[CloudView] Loaded with original color, points: %zu\n", rgbCloud.size()); + } else { + m_glWidget->addPointCloud(m_originalCloud, cloudName); + LOG_INFO("[CloudView] Loaded without color (color table), points: %zu\n", m_originalCloud.size()); + } + + // 保存线信息(用于旋转功能) + int lineCount = m_converter->getLoadedLineCount(); + if (lineCount > 0) { + m_currentLineNum = lineCount; + m_currentLinePtNum = static_cast(m_converter->getLoadedPointCount()) / lineCount; + } else { + m_currentLineNum = 0; + m_currentLinePtNum = 0; + } + + // 添加到列表 + QString itemText; + if (lineCount > 0) { + itemText = QString("%1 - %2 点, %3 线").arg(cloudName).arg(m_converter->getLoadedPointCount()).arg(lineCount); + } else { + itemText = QString("%1 - %2 点").arg(cloudName).arg(m_converter->getLoadedPointCount()); + } + if (hadColor) { + itemText += " [彩色]"; + } + m_cloudList->addItem(itemText); + + statusBar()->showMessage(QString("已加载 %1 个点%2").arg(m_converter->getLoadedPointCount()).arg(hadColor ? " (彩色)" : "")); + return true; +} + +bool CloudViewMainWindow::loadSegmentFile(const QString& fileName) +{ + statusBar()->showMessage("正在加载线段..."); + + LaserDataLoader loader; + std::vector> polyLines; + int result = loader.LoadPolySegments(fileName.toStdString(), polyLines); + + if (result != 0) { + LOG_INFO("[CloudView] Load segments failed: %s\n", loader.GetLastError().c_str()); + return false; + } + + if (polyLines.empty()) { + return false; + } + + // 将折线点转换为线段(使用点自带的颜色) + QVector segments; + for (const auto& polyLine : polyLines) { + for (size_t i = 0; i + 1 < polyLine.size(); ++i) { + const auto& p1 = polyLine[i]; + const auto& p2 = polyLine[i + 1]; + // 取起点颜色作为线段颜色,nRGB 为 (A<<24)|BGR 打包格式 + float r = ((p1.nRGB >> 0) & 0xFF) / 255.0f; + float g = ((p1.nRGB >> 8) & 0xFF) / 255.0f; + float b = ((p1.nRGB >> 16) & 0xFF) / 255.0f; + uint8_t a = static_cast((p1.nRGB >> 24) & 0xFF); + // A > 1 时作为线宽使用 + float lineWidth = (a > 1) ? static_cast(a) : 0.0f; + segments.append(LineSegment( + p1.x, p1.y, p1.z, + p2.x, p2.y, p2.z, + r, g, b, lineWidth)); + } + } + + if (segments.isEmpty()) { + return false; + } + + m_glWidget->addLineSegments(segments); + + // 添加到列表 + QFileInfo fileInfo(fileName); + int totalPolyCount = static_cast(polyLines.size()); + QString itemName = QString("Segments (%1)").arg(fileInfo.fileName()); + m_cloudList->addItem(QString("%1 - %2 条折线, %3 条线段") + .arg(itemName).arg(totalPolyCount).arg(segments.size())); + + statusBar()->showMessage(QString("已加载 %1 条折线, %2 条线段").arg(totalPolyCount).arg(segments.size())); + LOG_INFO("[CloudView] Loaded %d polylines, %d segments from %s\n", + totalPolyCount, segments.size(), fileName.toStdString().c_str()); + return true; +} + +void CloudViewMainWindow::onOpenPoseFile() +{ + QString fileName = QFileDialog::getOpenFileName( + this, + "打开姿态点文件", + QString(), + "文本文件 (*.txt);;所有文件 (*.*)" + ); + + if (fileName.isEmpty()) { + return; + } + + statusBar()->showMessage("正在加载姿态点..."); + + QFile file(fileName); + #if 0 + if (false) { + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + QMessageBox::critical(this, "错误", QString("无法打开文件: %1").arg(fileName)); + return; + } + + QTextStream in(&file); + const QString matrixText = in.readAll(); + file.close(); + + QMatrix4x4 matrix; + QString errorMessage; + if (!parseMatrixText(matrixText, matrix, &errorMessage)) { + QMessageBox::warning(this, "格式错误", errorMessage); + return; + } + + m_matrixEdit->setPlainText(formatMatrixText(matrix)); + statusBar()->showMessage(QString("已从 %1 加载矩阵").arg(QFileInfo(fileName).fileName())); + LOG_INFO("[CloudView] Loaded matrix from %s\n", fileName.toStdString().c_str()); + return; + } + #endif + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + QMessageBox::critical(this, "错误", QString("无法打开文件: %1").arg(fileName)); + statusBar()->showMessage("加载失败"); + return; + } + + QVector poses; + QTextStream in(&file); + int lineNum = 0; + int validCount = 0; + + while (!in.atEnd()) { + QString line = in.readLine().trimmed(); + lineNum++; + + // 跳过空行和注释 + if (line.isEmpty() || line.startsWith('#')) { + continue; + } + + // 解析格式:{x,y,z}-{r,p,y} + QRegExp regex("\\{([^}]+)\\}-\\{([^}]+)\\}"); + if (regex.indexIn(line) == -1) { + LOG_WARN("[CloudView] Line %d: Invalid format, expected {x,y,z}-{r,p,y}\n", lineNum); + continue; + } + + QString posStr = regex.cap(1); + QString rotStr = regex.cap(2); + + QStringList pos = posStr.split(','); + QStringList rot = rotStr.split(','); + + if (pos.size() != 3 || rot.size() != 3) { + LOG_WARN("[CloudView] Line %d: Invalid point format\n", lineNum); + continue; + } + + bool ok = true; + float x = pos[0].toFloat(&ok); if (!ok) continue; + float y = pos[1].toFloat(&ok); if (!ok) continue; + float z = pos[2].toFloat(&ok); if (!ok) continue; + float roll = rot[0].toFloat(&ok); if (!ok) continue; + float pitch = rot[1].toFloat(&ok); if (!ok) continue; + float yaw = rot[2].toFloat(&ok); if (!ok) continue; + + // 使用界面上配置的坐标轴长度 + bool scaleOk = true; + float scale = m_editPoseScale->text().toFloat(&scaleOk); + if (!scaleOk || scale <= 0) scale = 10.0f; + + poses.append(PosePoint(x, y, z, roll, pitch, yaw, scale)); + validCount++; + } + + file.close(); + + if (poses.isEmpty()) { + QMessageBox::warning(this, "警告", "文件中没有有效的姿态点数据"); + statusBar()->showMessage("加载失败"); + return; + } + + m_glWidget->addPosePoints(poses); + statusBar()->showMessage(QString("已加载 %1 个姿态点").arg(validCount)); + LOG_INFO("[CloudView] Loaded %d pose points from %s\n", validCount, fileName.toStdString().c_str()); +} + +void CloudViewMainWindow::onOpenBBoxFile() +{ + QString fileName = QFileDialog::getOpenFileName( + this, + "打开包围盒文件", + QString(), + "JSON 文件 (*.json);;所有文件 (*.*)" + ); + + if (fileName.isEmpty()) { + return; + } + + if (!loadBoundingBoxFile(fileName)) { + QMessageBox::critical(this, "错误", "无法加载包围盒数据"); + statusBar()->showMessage("加载失败"); + } +} + +bool CloudViewMainWindow::loadBoundingBoxFile(const QString& fileName) +{ + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly)) { + LOG_INFO("[CloudView] Cannot open bbox file: %s\n", fileName.toStdString().c_str()); + return false; + } + + QByteArray data = file.readAll(); + file.close(); + + QJsonParseError parseError; + QJsonDocument doc = QJsonDocument::fromJson(data, &parseError); + if (doc.isNull()) { + LOG_INFO("[CloudView] JSON parse error: %s\n", parseError.errorString().toStdString().c_str()); + return false; + } + + QJsonObject root = doc.object(); + QJsonArray records = root["records"].toArray(); + if (records.isEmpty()) { + LOG_INFO("[CloudView] No records in bbox file\n"); + return false; + } + + // 包围盒8个顶点的12条边索引 + // 底面: 0-1, 1-2, 2-3, 3-0 + // 顶面: 4-5, 5-6, 6-7, 7-4 + // 立柱: 0-4, 1-5, 2-6, 3-7 + static const int edgeIndices[][2] = { + {0, 1}, {1, 2}, {2, 3}, {3, 0}, // 底面 + {4, 5}, {5, 6}, {6, 7}, {7, 4}, // 顶面 + {0, 4}, {1, 5}, {2, 6}, {3, 7} // 立柱 + }; + + QVector segments; + QVector graspPoses; + int bboxCount = 0; + + // 为每个 record 使用不同的颜色 + const float colors[][3] = { + {0.0f, 1.0f, 0.0f}, // 绿色 + {1.0f, 1.0f, 0.0f}, // 黄色 + {0.0f, 1.0f, 1.0f}, // 青色 + {1.0f, 0.5f, 0.0f}, // 橙色 + {1.0f, 0.0f, 1.0f}, // 紫色 + {0.5f, 1.0f, 0.5f}, // 浅绿 + }; + const int colorCount = sizeof(colors) / sizeof(colors[0]); + + // 使用界面上配置的坐标轴长度 + bool poseScaleOk = true; + float poseScale = m_editPoseScale->text().toFloat(&poseScaleOk); + if (!poseScaleOk || poseScale <= 0) poseScale = 10.0f; + + for (int i = 0; i < records.size(); ++i) { + QJsonObject record = records[i].toObject(); + QJsonObject bbox = record["bounding_box"].toObject(); + QJsonArray vertices = bbox["vertices"].toArray(); + + if (vertices.size() != 8) { + LOG_WARN("[CloudView] Record %d: vertices count != 8, skip\n", i); + continue; + } + + // 解析8个顶点 + float vx[8], vy[8], vz[8]; + for (int j = 0; j < 8; ++j) { + QJsonObject v = vertices[j].toObject(); + vx[j] = static_cast(v["x"].toDouble()); + vy[j] = static_cast(v["y"].toDouble()); + vz[j] = static_cast(v["z"].toDouble()); + } + + // 选择颜色 + float r = colors[i % colorCount][0]; + float g = colors[i % colorCount][1]; + float b = colors[i % colorCount][2]; + + // 生成12条边的线段 + for (const auto& edge : edgeIndices) { + int a = edge[0]; + int b_idx = edge[1]; + segments.append(LineSegment( + vx[a], vy[a], vz[a], + vx[b_idx], vy[b_idx], vz[b_idx], + r, g, b, 2.0f)); + } + + bboxCount++; + + // 解析抓取点(右抓取点) + if (record.contains("has_right_point") && record["has_right_point"].toBool()) { + QJsonObject graspPt = record["right_grasp_point"].toObject(); + QJsonObject pos = graspPt["position"].toObject(); + QJsonObject ori = graspPt["orientation"].toObject(); + + float px = static_cast(pos["x"].toDouble()); + float py = static_cast(pos["y"].toDouble()); + float pz = static_cast(pos["z"].toDouble()); + float roll = static_cast(ori["roll"].toDouble()); + float pitch = static_cast(ori["pitch"].toDouble()); + float yaw = static_cast(ori["yaw"].toDouble()); + + graspPoses.append(PosePoint(px, py, pz, roll, pitch, yaw, poseScale)); + } + + // 解析左抓取点 + if (record.contains("has_left_point") && record["has_left_point"].toBool()) { + QJsonObject graspPt = record["left_grasp_point"].toObject(); + QJsonObject pos = graspPt["position"].toObject(); + QJsonObject ori = graspPt["orientation"].toObject(); + + float px = static_cast(pos["x"].toDouble()); + float py = static_cast(pos["y"].toDouble()); + float pz = static_cast(pos["z"].toDouble()); + float roll = static_cast(ori["roll"].toDouble()); + float pitch = static_cast(ori["pitch"].toDouble()); + float yaw = static_cast(ori["yaw"].toDouble()); + + graspPoses.append(PosePoint(px, py, pz, roll, pitch, yaw, poseScale)); + } + } + + if (segments.isEmpty()) { + return false; + } + + m_glWidget->addLineSegments(segments); + + if (!graspPoses.isEmpty()) { + m_glWidget->addPosePoints(graspPoses); + } + + // 添加到列表 + QFileInfo fileInfo(fileName); + QString itemName = QString("BBox (%1)").arg(fileInfo.fileName()); + QString itemText = QString("%1 - %2 个包围盒, %3 条边") + .arg(itemName).arg(bboxCount).arg(segments.size()); + if (!graspPoses.isEmpty()) { + itemText += QString(", %1 个抓取点").arg(graspPoses.size()); + } + m_cloudList->addItem(itemText); + + statusBar()->showMessage(QString("已加载 %1 个包围盒").arg(bboxCount)); + LOG_INFO("[CloudView] Loaded %d bounding boxes, %d grasp points from %s\n", + bboxCount, graspPoses.size(), fileName.toStdString().c_str()); + return true; +} + +void CloudViewMainWindow::applyTransformToAllClouds(const QMatrix4x4& matrix) +{ + m_glWidget->transformAllClouds(matrix); + transformPointCloud(m_originalCloud, matrix); + + m_glWidget->clearSelectedPoints(); + m_glWidget->clearSelectedLine(); + m_glWidget->clearListHighlightPoint(); + m_glWidget->clearLineSegments(); + m_glWidget->clearBasicShapeSegments(); + m_glWidget->clearPosePoints(); + + m_lblPoint1->setText("点1: --"); + m_lblPoint2->setText("点2: --"); + clearMeasureResultDisplay(); + m_editPoint1X->setText("--"); + m_editPoint1Y->setText("--"); + m_editPoint1Z->setText("--"); + m_editPoint2X->setText("--"); + m_editPoint2Y->setText("--"); + m_editPoint2Z->setText("--"); + m_lblLineIndex->setText("--"); + m_lblLinePointCount->setText("--"); + m_currentLinePoints.clear(); + + if (m_linePointsDialog && m_linePointsTable) { + updateLinePointsDialog(); + } +} + +void CloudViewMainWindow::addGeneratedCloud(const PointCloudXYZ& cloud, const QString& name) +{ + m_glWidget->clearSelectedPoints(); + m_glWidget->clearSelectedLine(); + m_glWidget->clearListHighlightPoint(); + + const QString cloudName = QString("Cloud_%1 (%2)").arg(++m_cloudCount).arg(name); + m_glWidget->addPointCloud(cloud, cloudName); + + m_originalCloud = cloud; + m_currentLineNum = 0; + m_currentLinePtNum = 0; + + m_cloudList->addItem(QString("%1 - %2 点 [生成]").arg(cloudName).arg(cloud.size())); + statusBar()->showMessage(QString("已生成点云: %1,点数 %2").arg(name).arg(cloud.size())); + LOG_INFO("[CloudView] Generated synthetic cloud: %s, points=%zu\n", + cloudName.toStdString().c_str(), cloud.size()); +} + +void CloudViewMainWindow::showBasicShape(const QString& shapeName, + const QVector& segments, + const QVector& points) { - statusBar()->showMessage("正在加载点云..."); - - QFileInfo fileInfo(fileName); - QString ext = fileInfo.suffix().toLower(); - QString cloudName = QString("Cloud_%1 (%2)").arg(++m_cloudCount).arg(fileInfo.fileName()); - - // 统一使用 PointCloudXYZRGB 加载,支持带颜色和不带颜色的文件 - PointCloudXYZRGB rgbCloud; - int result = m_converter->loadFromFile(fileName.toStdString(), rgbCloud); - - if (result != 0) { - LOG_INFO("[CloudView] Load point cloud failed: %s\n", m_converter->getLastError().c_str()); - return false; - } - - // 保存原始完整点云 XYZ(用于旋转/线上点等功能) - m_originalCloud.clear(); - m_originalCloud.reserve(rgbCloud.size()); - for (size_t i = 0; i < rgbCloud.points.size(); ++i) { - const auto& pt = rgbCloud.points[i]; - Point3D xyzPt(pt.x, pt.y, pt.z); - int lineIdx = (i < rgbCloud.lineIndices.size()) ? rgbCloud.lineIndices[i] : 0; - m_originalCloud.push_back(xyzPt, lineIdx); - } - - // 根据是否有颜色选择显示方式 - bool hadColor = m_converter->lastLoadHadColor(); - - if (ext == "pcd" || ext == "ply") { - // PCD/PLY 文件始终走 XYZRGB 路径,避免被颜色轮换表染成蓝色等 - if (!hadColor) { - // 无 rgb 字段:默认灰色显示 - for (size_t i = 0; i < rgbCloud.points.size(); ++i) { - rgbCloud.points[i].r = 192; - rgbCloud.points[i].g = 192; - rgbCloud.points[i].b = 192; - } - } - m_glWidget->addPointCloud(rgbCloud, cloudName); - LOG_INFO("[CloudView] PCD/PLY loaded with XYZRGB path (hasRgbField=%d), points: %zu\n", - m_converter->lastLoadHadColor(), rgbCloud.size()); - } else if (hadColor) { - m_glWidget->addPointCloud(rgbCloud, cloudName); - LOG_INFO("[CloudView] Loaded with original color, points: %zu\n", rgbCloud.size()); - } else { - m_glWidget->addPointCloud(m_originalCloud, cloudName); - LOG_INFO("[CloudView] Loaded without color (color table), points: %zu\n", m_originalCloud.size()); - } - - // 保存线信息(用于旋转功能) - int lineCount = m_converter->getLoadedLineCount(); - if (lineCount > 0) { - m_currentLineNum = lineCount; - m_currentLinePtNum = static_cast(m_converter->getLoadedPointCount()) / lineCount; - } else { - m_currentLineNum = 0; - m_currentLinePtNum = 0; - } - - // 添加到列表 - QString itemText; - if (lineCount > 0) { - itemText = QString("%1 - %2 点, %3 线").arg(cloudName).arg(m_converter->getLoadedPointCount()).arg(lineCount); - } else { - itemText = QString("%1 - %2 点").arg(cloudName).arg(m_converter->getLoadedPointCount()); - } - if (hadColor) { - itemText += " [彩色]"; - } - m_cloudList->addItem(itemText); - - statusBar()->showMessage(QString("已加载 %1 个点%2").arg(m_converter->getLoadedPointCount()).arg(hadColor ? " (彩色)" : "")); - return true; -} - -bool CloudViewMainWindow::loadSegmentFile(const QString& fileName) -{ - statusBar()->showMessage("正在加载线段..."); - - LaserDataLoader loader; - std::vector> polyLines; - int result = loader.LoadPolySegments(fileName.toStdString(), polyLines); - - if (result != 0) { - LOG_INFO("[CloudView] Load segments failed: %s\n", loader.GetLastError().c_str()); - return false; - } - - if (polyLines.empty()) { - return false; - } - - // 将折线点转换为线段(使用点自带的颜色) - QVector segments; - for (const auto& polyLine : polyLines) { - for (size_t i = 0; i + 1 < polyLine.size(); ++i) { - const auto& p1 = polyLine[i]; - const auto& p2 = polyLine[i + 1]; - // 取起点颜色作为线段颜色,nRGB 为 (A<<24)|BGR 打包格式 - float r = ((p1.nRGB >> 0) & 0xFF) / 255.0f; - float g = ((p1.nRGB >> 8) & 0xFF) / 255.0f; - float b = ((p1.nRGB >> 16) & 0xFF) / 255.0f; - uint8_t a = static_cast((p1.nRGB >> 24) & 0xFF); - // A > 1 时作为线宽使用 - float lineWidth = (a > 1) ? static_cast(a) : 0.0f; - segments.append(LineSegment( - p1.x, p1.y, p1.z, - p2.x, p2.y, p2.z, - r, g, b, lineWidth)); - } - } - - if (segments.isEmpty()) { - return false; - } - - m_glWidget->addLineSegments(segments); - - // 添加到列表 - QFileInfo fileInfo(fileName); - int totalPolyCount = static_cast(polyLines.size()); - QString itemName = QString("Segments (%1)").arg(fileInfo.fileName()); - m_cloudList->addItem(QString("%1 - %2 条折线, %3 条线段") - .arg(itemName).arg(totalPolyCount).arg(segments.size())); - - statusBar()->showMessage(QString("已加载 %1 条折线, %2 条线段").arg(totalPolyCount).arg(segments.size())); - LOG_INFO("[CloudView] Loaded %d polylines, %d segments from %s\n", - totalPolyCount, segments.size(), fileName.toStdString().c_str()); - return true; -} - -void CloudViewMainWindow::onOpenPoseFile() -{ - QString fileName = QFileDialog::getOpenFileName( - this, - "打开姿态点文件", - QString(), - "文本文件 (*.txt);;所有文件 (*.*)" - ); - - if (fileName.isEmpty()) { + if (segments.isEmpty() && points.isEmpty()) { return; } - statusBar()->showMessage("正在加载姿态点..."); - - QFile file(fileName); - #if 0 - if (false) { - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - QMessageBox::critical(this, "错误", QString("无法打开文件: %1").arg(fileName)); - return; - } - - QTextStream in(&file); - const QString matrixText = in.readAll(); - file.close(); - - QMatrix4x4 matrix; - QString errorMessage; - if (!parseMatrixText(matrixText, matrix, &errorMessage)) { - QMessageBox::warning(this, "格式错误", errorMessage); - return; - } - - m_matrixEdit->setPlainText(formatMatrixText(matrix)); - statusBar()->showMessage(QString("已从 %1 加载矩阵").arg(QFileInfo(fileName).fileName())); - LOG_INFO("[CloudView] Loaded matrix from %s\n", fileName.toStdString().c_str()); - return; + if (!segments.isEmpty()) { + m_glWidget->appendBasicShapeSegments(segments); } - #endif - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - QMessageBox::critical(this, "错误", QString("无法打开文件: %1").arg(fileName)); - statusBar()->showMessage("加载失败"); - return; + if (!points.isEmpty()) { + m_glWidget->appendBasicShapePoints(points); } - - QVector poses; - QTextStream in(&file); - int lineNum = 0; - int validCount = 0; - - while (!in.atEnd()) { - QString line = in.readLine().trimmed(); - lineNum++; - - // 跳过空行和注释 - if (line.isEmpty() || line.startsWith('#')) { - continue; - } - - // 解析格式:{x,y,z}-{r,p,y} - QRegExp regex("\\{([^}]+)\\}-\\{([^}]+)\\}"); - if (regex.indexIn(line) == -1) { - LOG_WARN("[CloudView] Line %d: Invalid format, expected {x,y,z}-{r,p,y}\n", lineNum); - continue; - } - - QString posStr = regex.cap(1); - QString rotStr = regex.cap(2); - - QStringList pos = posStr.split(','); - QStringList rot = rotStr.split(','); - - if (pos.size() != 3 || rot.size() != 3) { - LOG_WARN("[CloudView] Line %d: Invalid point format\n", lineNum); - continue; - } - - bool ok = true; - float x = pos[0].toFloat(&ok); if (!ok) continue; - float y = pos[1].toFloat(&ok); if (!ok) continue; - float z = pos[2].toFloat(&ok); if (!ok) continue; - float roll = rot[0].toFloat(&ok); if (!ok) continue; - float pitch = rot[1].toFloat(&ok); if (!ok) continue; - float yaw = rot[2].toFloat(&ok); if (!ok) continue; - - // 使用界面上配置的坐标轴长度 - bool scaleOk = true; - float scale = m_editPoseScale->text().toFloat(&scaleOk); - if (!scaleOk || scale <= 0) scale = 10.0f; - - poses.append(PosePoint(x, y, z, roll, pitch, yaw, scale)); - validCount++; - } - - file.close(); - - if (poses.isEmpty()) { - QMessageBox::warning(this, "警告", "文件中没有有效的姿态点数据"); - statusBar()->showMessage("加载失败"); - return; - } - - m_glWidget->addPosePoints(poses); - statusBar()->showMessage(QString("已加载 %1 个姿态点").arg(validCount)); - LOG_INFO("[CloudView] Loaded %d pose points from %s\n", validCount, fileName.toStdString().c_str()); -} - -void CloudViewMainWindow::onOpenBBoxFile() -{ - QString fileName = QFileDialog::getOpenFileName( - this, - "打开包围盒文件", - QString(), - "JSON 文件 (*.json);;所有文件 (*.*)" - ); - - if (fileName.isEmpty()) { - return; - } - - if (!loadBoundingBoxFile(fileName)) { - QMessageBox::critical(this, "错误", "无法加载包围盒数据"); - statusBar()->showMessage("加载失败"); - } -} - -bool CloudViewMainWindow::loadBoundingBoxFile(const QString& fileName) -{ - QFile file(fileName); - if (!file.open(QIODevice::ReadOnly)) { - LOG_INFO("[CloudView] Cannot open bbox file: %s\n", fileName.toStdString().c_str()); - return false; - } - - QByteArray data = file.readAll(); - file.close(); - - QJsonParseError parseError; - QJsonDocument doc = QJsonDocument::fromJson(data, &parseError); - if (doc.isNull()) { - LOG_INFO("[CloudView] JSON parse error: %s\n", parseError.errorString().toStdString().c_str()); - return false; - } - - QJsonObject root = doc.object(); - QJsonArray records = root["records"].toArray(); - if (records.isEmpty()) { - LOG_INFO("[CloudView] No records in bbox file\n"); - return false; - } - - // 包围盒8个顶点的12条边索引 - // 底面: 0-1, 1-2, 2-3, 3-0 - // 顶面: 4-5, 5-6, 6-7, 7-4 - // 立柱: 0-4, 1-5, 2-6, 3-7 - static const int edgeIndices[][2] = { - {0, 1}, {1, 2}, {2, 3}, {3, 0}, // 底面 - {4, 5}, {5, 6}, {6, 7}, {7, 4}, // 顶面 - {0, 4}, {1, 5}, {2, 6}, {3, 7} // 立柱 - }; - - QVector segments; - QVector graspPoses; - int bboxCount = 0; - - // 为每个 record 使用不同的颜色 - const float colors[][3] = { - {0.0f, 1.0f, 0.0f}, // 绿色 - {1.0f, 1.0f, 0.0f}, // 黄色 - {0.0f, 1.0f, 1.0f}, // 青色 - {1.0f, 0.5f, 0.0f}, // 橙色 - {1.0f, 0.0f, 1.0f}, // 紫色 - {0.5f, 1.0f, 0.5f}, // 浅绿 - }; - const int colorCount = sizeof(colors) / sizeof(colors[0]); - - // 使用界面上配置的坐标轴长度 - bool poseScaleOk = true; - float poseScale = m_editPoseScale->text().toFloat(&poseScaleOk); - if (!poseScaleOk || poseScale <= 0) poseScale = 10.0f; - - for (int i = 0; i < records.size(); ++i) { - QJsonObject record = records[i].toObject(); - QJsonObject bbox = record["bounding_box"].toObject(); - QJsonArray vertices = bbox["vertices"].toArray(); - - if (vertices.size() != 8) { - LOG_WARN("[CloudView] Record %d: vertices count != 8, skip\n", i); - continue; - } - - // 解析8个顶点 - float vx[8], vy[8], vz[8]; - for (int j = 0; j < 8; ++j) { - QJsonObject v = vertices[j].toObject(); - vx[j] = static_cast(v["x"].toDouble()); - vy[j] = static_cast(v["y"].toDouble()); - vz[j] = static_cast(v["z"].toDouble()); - } - - // 选择颜色 - float r = colors[i % colorCount][0]; - float g = colors[i % colorCount][1]; - float b = colors[i % colorCount][2]; - - // 生成12条边的线段 - for (const auto& edge : edgeIndices) { - int a = edge[0]; - int b_idx = edge[1]; - segments.append(LineSegment( - vx[a], vy[a], vz[a], - vx[b_idx], vy[b_idx], vz[b_idx], - r, g, b, 2.0f)); - } - - bboxCount++; - - // 解析抓取点(右抓取点) - if (record.contains("has_right_point") && record["has_right_point"].toBool()) { - QJsonObject graspPt = record["right_grasp_point"].toObject(); - QJsonObject pos = graspPt["position"].toObject(); - QJsonObject ori = graspPt["orientation"].toObject(); - - float px = static_cast(pos["x"].toDouble()); - float py = static_cast(pos["y"].toDouble()); - float pz = static_cast(pos["z"].toDouble()); - float roll = static_cast(ori["roll"].toDouble()); - float pitch = static_cast(ori["pitch"].toDouble()); - float yaw = static_cast(ori["yaw"].toDouble()); - - graspPoses.append(PosePoint(px, py, pz, roll, pitch, yaw, poseScale)); - } - - // 解析左抓取点 - if (record.contains("has_left_point") && record["has_left_point"].toBool()) { - QJsonObject graspPt = record["left_grasp_point"].toObject(); - QJsonObject pos = graspPt["position"].toObject(); - QJsonObject ori = graspPt["orientation"].toObject(); - - float px = static_cast(pos["x"].toDouble()); - float py = static_cast(pos["y"].toDouble()); - float pz = static_cast(pos["z"].toDouble()); - float roll = static_cast(ori["roll"].toDouble()); - float pitch = static_cast(ori["pitch"].toDouble()); - float yaw = static_cast(ori["yaw"].toDouble()); - - graspPoses.append(PosePoint(px, py, pz, roll, pitch, yaw, poseScale)); - } - } - - if (segments.isEmpty()) { - return false; - } - - m_glWidget->addLineSegments(segments); - - if (!graspPoses.isEmpty()) { - m_glWidget->addPosePoints(graspPoses); - } - - // 添加到列表 - QFileInfo fileInfo(fileName); - QString itemName = QString("BBox (%1)").arg(fileInfo.fileName()); - QString itemText = QString("%1 - %2 个包围盒, %3 条边") - .arg(itemName).arg(bboxCount).arg(segments.size()); - if (!graspPoses.isEmpty()) { - itemText += QString(", %1 个抓取点").arg(graspPoses.size()); - } - m_cloudList->addItem(itemText); - - statusBar()->showMessage(QString("已加载 %1 个包围盒").arg(bboxCount)); - LOG_INFO("[CloudView] Loaded %d bounding boxes, %d grasp points from %s\n", - bboxCount, graspPoses.size(), fileName.toStdString().c_str()); - return true; -} - -void CloudViewMainWindow::applyTransformToAllClouds(const QMatrix4x4& matrix) -{ - m_glWidget->transformAllClouds(matrix); - transformPointCloud(m_originalCloud, matrix); - - m_glWidget->clearSelectedPoints(); - m_glWidget->clearSelectedLine(); - m_glWidget->clearListHighlightPoint(); - m_glWidget->clearLineSegments(); - m_glWidget->clearBasicShapeSegments(); - m_glWidget->clearPosePoints(); - - m_lblPoint1->setText("点1: --"); - m_lblPoint2->setText("点2: --"); - m_lblDistance->setText("--"); - m_editPoint1X->setText("--"); - m_editPoint1Y->setText("--"); - m_editPoint1Z->setText("--"); - m_editPoint2X->setText("--"); - m_editPoint2Y->setText("--"); - m_editPoint2Z->setText("--"); - m_lblLineIndex->setText("--"); - m_lblLinePointCount->setText("--"); - m_currentLinePoints.clear(); - - if (m_linePointsDialog && m_linePointsTable) { - updateLinePointsDialog(); - } -} - -void CloudViewMainWindow::addGeneratedCloud(const PointCloudXYZ& cloud, const QString& name) -{ - m_glWidget->clearSelectedPoints(); - m_glWidget->clearSelectedLine(); - m_glWidget->clearListHighlightPoint(); - - const QString cloudName = QString("Cloud_%1 (%2)").arg(++m_cloudCount).arg(name); - m_glWidget->addPointCloud(cloud, cloudName); - - m_originalCloud = cloud; - m_currentLineNum = 0; - m_currentLinePtNum = 0; - - m_cloudList->addItem(QString("%1 - %2 点 [生成]").arg(cloudName).arg(cloud.size())); - statusBar()->showMessage(QString("已生成点云: %1,点数 %2").arg(name).arg(cloud.size())); - LOG_INFO("[CloudView] Generated synthetic cloud: %s, points=%zu\n", - cloudName.toStdString().c_str(), cloud.size()); -} - -void CloudViewMainWindow::showBasicShape(const QString& shapeName, const QVector& segments) -{ - if (segments.isEmpty()) { - return; - } - - m_glWidget->appendBasicShapeSegments(segments); statusBar()->showMessage(QString("已添加 %1").arg(shapeName)); - LOG_INFO("[CloudView] Add basic shape: %s, segments=%d\n", - shapeName.toStdString().c_str(), segments.size()); + LOG_INFO("[CloudView] Add basic shape: %s, segments=%d, points=%d\n", + shapeName.toStdString().c_str(), segments.size(), points.size()); } void CloudViewMainWindow::showBasicSurface(const QString& shapeName, const QVector& surfaces, const QVector& outlines) @@ -2343,11 +2402,11 @@ void CloudViewMainWindow::onShowCircleShape() QVector3D center; QVector3D size; getShapeFrame(m_originalCloud, center, size); - const double defaultRadius = std::max(1.0f, std::max(size.x(), size.y()) * 0.25f); - - QDialog dialog(this); - dialog.setWindowTitle("圆"); - dialog.setModal(true); + const double defaultRadius = std::max(1.0f, std::max(size.x(), size.y()) * 0.25f); + + QDialog dialog(this); + dialog.setWindowTitle("圆"); + dialog.setModal(true); QVBoxLayout* layout = new QVBoxLayout(&dialog); @@ -2355,14 +2414,14 @@ void CloudViewMainWindow::onShowCircleShape() tip->setWordWrap(true); tip->setStyleSheet("color: gray;"); layout->addWidget(tip); - - QGroupBox* centerGroup = new QGroupBox("圆心", &dialog); - QFormLayout* centerLayout = new QFormLayout(centerGroup); - QDoubleSpinBox* centerX = createShapeSpinBox(centerGroup, center.x()); - QDoubleSpinBox* centerY = createShapeSpinBox(centerGroup, center.y()); - QDoubleSpinBox* centerZ = createShapeSpinBox(centerGroup, center.z()); - centerLayout->addRow("X:", centerX); - centerLayout->addRow("Y:", centerY); + + QGroupBox* centerGroup = new QGroupBox("圆心", &dialog); + QFormLayout* centerLayout = new QFormLayout(centerGroup); + QDoubleSpinBox* centerX = createShapeSpinBox(centerGroup, center.x()); + QDoubleSpinBox* centerY = createShapeSpinBox(centerGroup, center.y()); + QDoubleSpinBox* centerZ = createShapeSpinBox(centerGroup, center.z()); + centerLayout->addRow("X:", centerX); + centerLayout->addRow("Y:", centerY); centerLayout->addRow("Z:", centerZ); layout->addWidget(centerGroup); @@ -2386,12 +2445,14 @@ void CloudViewMainWindow::onShowCircleShape() const QVector3D inputCenter(static_cast(centerX->value()), static_cast(centerY->value()), static_cast(centerZ->value())); - showBasicShape("圆", makeCircleSegments(inputCenter, - poseR->value(), - poseP->value(), - poseY->value(), - static_cast(radius->value()), - currentShapeEulerOrder(m_comboEulerOrder))); + showBasicShape("圆", + makeCircleSegments(inputCenter, + poseR->value(), + poseP->value(), + poseY->value(), + static_cast(radius->value()), + currentShapeEulerOrder(m_comboEulerOrder)), + makeShapeCenterPoint(inputCenter)); }; QPushButton* loadButton = new QPushButton("加载文件", &dialog); @@ -2399,7 +2460,7 @@ void CloudViewMainWindow::onShowCircleShape() const QString fileName = QFileDialog::getOpenFileName( this, "加载圆数据", QString(), "数据文件 (*.txt *.json *.dat);;所有文件 (*.*)"); - if (fileName.isEmpty()) { + if (fileName.isEmpty()) { return; } @@ -2411,8 +2472,9 @@ void CloudViewMainWindow::onShowCircleShape() } QVector segments; + QVector centerPoints; int shapeCount = 0; - if (!makeCircleSegmentsFromRows(rows, currentShapeEulerOrder(m_comboEulerOrder), segments, shapeCount, errorMessage)) { + if (!makeCircleSegmentsFromRows(rows, currentShapeEulerOrder(m_comboEulerOrder), segments, centerPoints, shapeCount, errorMessage)) { QMessageBox::warning(this, "错误", errorMessage); return; } @@ -2428,7 +2490,7 @@ void CloudViewMainWindow::onShowCircleShape() radius->setValue(values[6]); } - showBasicShape(QString("%1 个圆").arg(shapeCount), segments); + showBasicShape(QString("%1 个圆").arg(shapeCount), segments, centerPoints); }); QPushButton* addButton = new QPushButton("添加", &dialog); @@ -2446,33 +2508,33 @@ void CloudViewMainWindow::onShowCircleShape() dialog.exec(); } - + void CloudViewMainWindow::onShowRectangleShape() { QVector3D center; QVector3D size; - getShapeFrame(m_originalCloud, center, size); - const double defaultWidth = std::max(1.0f, size.x() * 0.5f); - const double defaultHeight = std::max(1.0f, size.y() * 0.5f); - - QDialog dialog(this); - dialog.setWindowTitle("矩形"); - dialog.setModal(true); - - QVBoxLayout* layout = new QVBoxLayout(&dialog); - + getShapeFrame(m_originalCloud, center, size); + const double defaultWidth = std::max(1.0f, size.x() * 0.5f); + const double defaultHeight = std::max(1.0f, size.y() * 0.5f); + + QDialog dialog(this); + dialog.setWindowTitle("矩形"); + dialog.setModal(true); + + QVBoxLayout* layout = new QVBoxLayout(&dialog); + QLabel* tip = new QLabel("输入矩形中心、姿态和宽高后点击添加;文件每行一个:{x,y,z}-{r,p,y}-{w,h}。", &dialog); tip->setWordWrap(true); tip->setStyleSheet("color: gray;"); layout->addWidget(tip); - - QGroupBox* centerGroup = new QGroupBox("中心", &dialog); - QFormLayout* centerLayout = new QFormLayout(centerGroup); - QDoubleSpinBox* centerX = createShapeSpinBox(centerGroup, center.x()); - QDoubleSpinBox* centerY = createShapeSpinBox(centerGroup, center.y()); - QDoubleSpinBox* centerZ = createShapeSpinBox(centerGroup, center.z()); - centerLayout->addRow("X:", centerX); - centerLayout->addRow("Y:", centerY); + + QGroupBox* centerGroup = new QGroupBox("中心", &dialog); + QFormLayout* centerLayout = new QFormLayout(centerGroup); + QDoubleSpinBox* centerX = createShapeSpinBox(centerGroup, center.x()); + QDoubleSpinBox* centerY = createShapeSpinBox(centerGroup, center.y()); + QDoubleSpinBox* centerZ = createShapeSpinBox(centerGroup, center.z()); + centerLayout->addRow("X:", centerX); + centerLayout->addRow("Y:", centerY); centerLayout->addRow("Z:", centerZ); layout->addWidget(centerGroup); @@ -2498,13 +2560,15 @@ void CloudViewMainWindow::onShowRectangleShape() const QVector3D inputCenter(static_cast(centerX->value()), static_cast(centerY->value()), static_cast(centerZ->value())); - showBasicShape("矩形", makeRectangleSegments(inputCenter, - poseR->value(), - poseP->value(), - poseY->value(), - static_cast(width->value()), - static_cast(height->value()), - currentShapeEulerOrder(m_comboEulerOrder))); + showBasicShape("矩形", + makeRectangleSegments(inputCenter, + poseR->value(), + poseP->value(), + poseY->value(), + static_cast(width->value()), + static_cast(height->value()), + currentShapeEulerOrder(m_comboEulerOrder)), + makeShapeCenterPoint(inputCenter)); }; QPushButton* loadButton = new QPushButton("加载文件", &dialog); @@ -2512,7 +2576,7 @@ void CloudViewMainWindow::onShowRectangleShape() const QString fileName = QFileDialog::getOpenFileName( this, "加载矩形数据", QString(), "数据文件 (*.txt *.json *.dat);;所有文件 (*.*)"); - if (fileName.isEmpty()) { + if (fileName.isEmpty()) { return; } @@ -2524,8 +2588,9 @@ void CloudViewMainWindow::onShowRectangleShape() } QVector segments; + QVector centerPoints; int shapeCount = 0; - if (!makeRectangleSegmentsFromRows(rows, currentShapeEulerOrder(m_comboEulerOrder), segments, shapeCount, errorMessage)) { + if (!makeRectangleSegmentsFromRows(rows, currentShapeEulerOrder(m_comboEulerOrder), segments, centerPoints, shapeCount, errorMessage)) { QMessageBox::warning(this, "错误", errorMessage); return; } @@ -2542,7 +2607,7 @@ void CloudViewMainWindow::onShowRectangleShape() height->setValue(values[7]); } - showBasicShape(QString("%1 个矩形").arg(shapeCount), segments); + showBasicShape(QString("%1 个矩形").arg(shapeCount), segments, centerPoints); }); QPushButton* addButton = new QPushButton("添加", &dialog); @@ -2689,46 +2754,46 @@ void CloudViewMainWindow::onShowSurfaceShape() void CloudViewMainWindow::onShowTriangleShape() { QVector3D center; - QVector3D size; - getShapeFrame(m_originalCloud, center, size); - const float radius = std::max(1.0f, std::max(size.x(), size.y()) * 0.3f); - - const QVector3D defaultP1(center.x(), center.y() - radius, center.z()); - const QVector3D defaultP2(center.x() + radius * 0.8660254f, center.y() + radius * 0.5f, center.z()); - const QVector3D defaultP3(center.x() - radius * 0.8660254f, center.y() + radius * 0.5f, center.z()); - - QDialog dialog(this); - dialog.setWindowTitle("三角形"); - dialog.setModal(true); - - QVBoxLayout* layout = new QVBoxLayout(&dialog); - + QVector3D size; + getShapeFrame(m_originalCloud, center, size); + const float radius = std::max(1.0f, std::max(size.x(), size.y()) * 0.3f); + + const QVector3D defaultP1(center.x(), center.y() - radius, center.z()); + const QVector3D defaultP2(center.x() + radius * 0.8660254f, center.y() + radius * 0.5f, center.z()); + const QVector3D defaultP3(center.x() - radius * 0.8660254f, center.y() + radius * 0.5f, center.z()); + + QDialog dialog(this); + dialog.setWindowTitle("三角形"); + dialog.setModal(true); + + QVBoxLayout* layout = new QVBoxLayout(&dialog); + QLabel* tip = new QLabel("输入三个顶点后点击添加;文件每行一个:{x,y,z}-{x,y,z}-{x,y,z}。", &dialog); - tip->setWordWrap(true); - tip->setStyleSheet("color: gray;"); - layout->addWidget(tip); - - auto addPointGroup = [&](const QString& title, const QVector3D& value, QDoubleSpinBox*& x, QDoubleSpinBox*& y, QDoubleSpinBox*& z) { - QGroupBox* group = new QGroupBox(title, &dialog); - QFormLayout* form = new QFormLayout(group); - x = createShapeSpinBox(group, value.x()); - y = createShapeSpinBox(group, value.y()); - z = createShapeSpinBox(group, value.z()); - form->addRow("X:", x); - form->addRow("Y:", y); - form->addRow("Z:", z); - layout->addWidget(group); - }; - - QDoubleSpinBox* x1 = nullptr; - QDoubleSpinBox* y1 = nullptr; - QDoubleSpinBox* z1 = nullptr; - QDoubleSpinBox* x2 = nullptr; - QDoubleSpinBox* y2 = nullptr; - QDoubleSpinBox* z2 = nullptr; - QDoubleSpinBox* x3 = nullptr; - QDoubleSpinBox* y3 = nullptr; - QDoubleSpinBox* z3 = nullptr; + tip->setWordWrap(true); + tip->setStyleSheet("color: gray;"); + layout->addWidget(tip); + + auto addPointGroup = [&](const QString& title, const QVector3D& value, QDoubleSpinBox*& x, QDoubleSpinBox*& y, QDoubleSpinBox*& z) { + QGroupBox* group = new QGroupBox(title, &dialog); + QFormLayout* form = new QFormLayout(group); + x = createShapeSpinBox(group, value.x()); + y = createShapeSpinBox(group, value.y()); + z = createShapeSpinBox(group, value.z()); + form->addRow("X:", x); + form->addRow("Y:", y); + form->addRow("Z:", z); + layout->addWidget(group); + }; + + QDoubleSpinBox* x1 = nullptr; + QDoubleSpinBox* y1 = nullptr; + QDoubleSpinBox* z1 = nullptr; + QDoubleSpinBox* x2 = nullptr; + QDoubleSpinBox* y2 = nullptr; + QDoubleSpinBox* z2 = nullptr; + QDoubleSpinBox* x3 = nullptr; + QDoubleSpinBox* y3 = nullptr; + QDoubleSpinBox* z3 = nullptr; addPointGroup("点1", defaultP1, x1, y1, z1); addPointGroup("点2", defaultP2, x2, y2, z2); addPointGroup("点3", defaultP3, x3, y3, z3); @@ -2747,7 +2812,7 @@ void CloudViewMainWindow::onShowTriangleShape() const QString fileName = QFileDialog::getOpenFileName( this, "加载三角形数据", QString(), "数据文件 (*.txt *.json *.dat);;所有文件 (*.*)"); - if (fileName.isEmpty()) { + if (fileName.isEmpty()) { return; } @@ -2796,1785 +2861,1817 @@ void CloudViewMainWindow::onShowTriangleShape() dialog.exec(); } - -void CloudViewMainWindow::onClearAll() -{ - m_glWidget->clearPointClouds(); - m_glWidget->clearLineSegments(); - m_glWidget->clearBasicShapeSegments(); - m_glWidget->clearPosePoints(); - m_cloudList->clear(); - m_cloudCount = 0; - m_currentLineNum = 0; - m_currentLinePtNum = 0; - m_originalCloud.clear(); - - // 清除选点信息 - m_lblPoint1->setText("点1: --"); - m_lblPoint2->setText("点2: --"); - m_lblDistance->setText("--"); - m_editPoint1X->setText("--"); - m_editPoint1Y->setText("--"); - m_editPoint1Z->setText("--"); - m_editPoint2X->setText("--"); - m_editPoint2Y->setText("--"); - m_editPoint2Z->setText("--"); - - // 清除选线信息 - m_lblLineIndex->setText("--"); - m_lblLinePointCount->setText("--"); - - statusBar()->showMessage("已清除所有数据"); -} - -void CloudViewMainWindow::onResetView() -{ - m_glWidget->resetView(); - statusBar()->showMessage("视图已重置"); -} - -void CloudViewMainWindow::onClearSelectedPoints() -{ - m_glWidget->clearSelectedPoints(); - m_glWidget->clearPosePoints(); // 清除选点时也清除姿态 - m_lblPoint1->setText("点1: --"); - m_lblPoint2->setText("点2: --"); - m_lblDistance->setText("--"); - m_editPoint1X->setText("--"); - m_editPoint1Y->setText("--"); - m_editPoint1Z->setText("--"); - m_editPoint2X->setText("--"); - m_editPoint2Y->setText("--"); - m_editPoint2Z->setText("--"); - statusBar()->showMessage("已清除选中的点"); -} - -void CloudViewMainWindow::onPointSelected(const SelectedPointInfo& point) -{ - if (!point.valid) { - return; - } - - // 选择新点时清除之前的姿态显示 - m_glWidget->clearPosePoints(); - - updateSelectedPointsDisplay(); - - // 状态栏显示:坐标、线号、索引号 - QString statusMsg = QString("选中点: (%1, %2, %3)") - .arg(point.x, 0, 'f', 3) - .arg(point.y, 0, 'f', 3) - .arg(point.z, 0, 'f', 3); - if (point.lineIndex >= 0) { - statusMsg += QString(" | 线号: %1").arg(point.lineIndex); - if (point.pointIndexInLine >= 0) { - statusMsg += QString(" | 索引号: %1").arg(point.pointIndexInLine); - } - } - statusBar()->showMessage(statusMsg); -} - + +void CloudViewMainWindow::onClearAll() +{ + m_glWidget->clearPointClouds(); + m_glWidget->clearLineSegments(); + m_glWidget->clearBasicShapeSegments(); + m_glWidget->clearPosePoints(); + m_cloudList->clear(); + m_cloudCount = 0; + m_currentLineNum = 0; + m_currentLinePtNum = 0; + m_originalCloud.clear(); + + // 清除选点信息 + m_lblPoint1->setText("点1: --"); + m_lblPoint2->setText("点2: --"); + clearMeasureResultDisplay(); + m_editPoint1X->setText("--"); + m_editPoint1Y->setText("--"); + m_editPoint1Z->setText("--"); + m_editPoint2X->setText("--"); + m_editPoint2Y->setText("--"); + m_editPoint2Z->setText("--"); + + // 清除选线信息 + m_lblLineIndex->setText("--"); + m_lblLinePointCount->setText("--"); + + statusBar()->showMessage("已清除所有数据"); +} + +void CloudViewMainWindow::onResetView() +{ + m_glWidget->resetView(); + statusBar()->showMessage("视图已重置"); +} + +void CloudViewMainWindow::onClearSelectedPoints() +{ + m_glWidget->clearSelectedPoints(); + m_glWidget->clearPosePoints(); // 清除选点时也清除姿态 + m_lblPoint1->setText("点1: --"); + m_lblPoint2->setText("点2: --"); + clearMeasureResultDisplay(); + m_editPoint1X->setText("--"); + m_editPoint1Y->setText("--"); + m_editPoint1Z->setText("--"); + m_editPoint2X->setText("--"); + m_editPoint2Y->setText("--"); + m_editPoint2Z->setText("--"); + statusBar()->showMessage("已清除选中的点"); +} + +void CloudViewMainWindow::onPointSelected(const SelectedPointInfo& point) +{ + if (!point.valid) { + return; + } + + // 选择新点时清除之前的姿态显示 + m_glWidget->clearPosePoints(); + + updateSelectedPointsDisplay(); + + // 状态栏显示:坐标、线号、索引号 + QString statusMsg = QString("选中点: (%1, %2, %3)") + .arg(point.x, 0, 'f', 3) + .arg(point.y, 0, 'f', 3) + .arg(point.z, 0, 'f', 3); + if (point.lineIndex >= 0) { + statusMsg += QString(" | 线号: %1").arg(point.lineIndex); + if (point.pointIndexInLine >= 0) { + statusMsg += QString(" | 索引号: %1").arg(point.pointIndexInLine); + } + } + statusBar()->showMessage(statusMsg); +} + void CloudViewMainWindow::onTwoPointsSelected(const SelectedPointInfo& p1, const SelectedPointInfo& p2, float distance) { updateSelectedPointsDisplay(); + updateMeasureResultDisplay(p1, p2, distance, "测量距离"); +} + +void CloudViewMainWindow::clearMeasureResultDisplay() +{ + m_lblDistance->setText("--"); + if (m_lblDeltaXYZ) { + m_lblDeltaXYZ->setText("--"); + } +} + +void CloudViewMainWindow::updateMeasureResultDisplay(const SelectedPointInfo& p1, + const SelectedPointInfo& p2, + float distance, + const QString& statusPrefix) +{ + const float dx = p2.x - p1.x; + const float dy = p2.y - p1.y; + const float dz = p2.z - p1.z; + m_lblDistance->setText(QString("%1 mm").arg(distance, 0, 'f', 3)); - statusBar()->showMessage(QString("测量距离: %1 mm").arg(distance, 0, 'f', 3)); + m_lblDeltaXYZ->setText(QString("X:%1 mm Y:%2 mm Z:%3 mm") + .arg(dx, 0, 'f', 3) + .arg(dy, 0, 'f', 3) + .arg(dz, 0, 'f', 3)); + + statusBar()->showMessage(QString("%1: %2 mm | X:%3 Y:%4 Z:%5") + .arg(statusPrefix) + .arg(distance, 0, 'f', 3) + .arg(dx, 0, 'f', 3) + .arg(dy, 0, 'f', 3) + .arg(dz, 0, 'f', 3)); } void CloudViewMainWindow::updateSelectedPointsDisplay() { auto selectedPoints = m_glWidget->getSelectedPoints(); - - if (selectedPoints.size() >= 1 && selectedPoints[0].valid) { - QString text; - if (selectedPoints[0].lineIndex >= 0) { - text = QString("点1: 线号:%1 点序:%2") - .arg(selectedPoints[0].lineIndex) - .arg(selectedPoints[0].pointIndexInLine); - } else { - text = QString("点1:"); - } - m_lblPoint1->setText(text); - - // 更新坐标编辑框 - m_editPoint1X->setText(QString::number(selectedPoints[0].x, 'f', 3)); - m_editPoint1Y->setText(QString::number(selectedPoints[0].y, 'f', 3)); - m_editPoint1Z->setText(QString::number(selectedPoints[0].z, 'f', 3)); - } else { - m_lblPoint1->setText("点1: --"); - m_editPoint1X->setText("--"); - m_editPoint1Y->setText("--"); - m_editPoint1Z->setText("--"); - } - - if (selectedPoints.size() >= 2 && selectedPoints[1].valid) { - QString text; - if (selectedPoints[1].lineIndex >= 0) { - text = QString("点2: 线号:%1 点序:%2") - .arg(selectedPoints[1].lineIndex) - .arg(selectedPoints[1].pointIndexInLine); - } else { - text = QString("点2:"); - } - m_lblPoint2->setText(text); - - // 更新坐标编辑框 - m_editPoint2X->setText(QString::number(selectedPoints[1].x, 'f', 3)); - m_editPoint2Y->setText(QString::number(selectedPoints[1].y, 'f', 3)); - m_editPoint2Z->setText(QString::number(selectedPoints[1].z, 'f', 3)); + + if (selectedPoints.size() >= 1 && selectedPoints[0].valid) { + QString text; + if (selectedPoints[0].lineIndex >= 0) { + text = QString("点1: 线号:%1 点序:%2") + .arg(selectedPoints[0].lineIndex) + .arg(selectedPoints[0].pointIndexInLine); + } else { + text = QString("点1:"); + } + m_lblPoint1->setText(text); + + // 更新坐标编辑框 + m_editPoint1X->setText(QString::number(selectedPoints[0].x, 'f', 3)); + m_editPoint1Y->setText(QString::number(selectedPoints[0].y, 'f', 3)); + m_editPoint1Z->setText(QString::number(selectedPoints[0].z, 'f', 3)); + } else { + m_lblPoint1->setText("点1: --"); + m_editPoint1X->setText("--"); + m_editPoint1Y->setText("--"); + m_editPoint1Z->setText("--"); + } + + if (selectedPoints.size() >= 2 && selectedPoints[1].valid) { + QString text; + if (selectedPoints[1].lineIndex >= 0) { + text = QString("点2: 线号:%1 点序:%2") + .arg(selectedPoints[1].lineIndex) + .arg(selectedPoints[1].pointIndexInLine); + } else { + text = QString("点2:"); + } + m_lblPoint2->setText(text); + + // 更新坐标编辑框 + m_editPoint2X->setText(QString::number(selectedPoints[1].x, 'f', 3)); + m_editPoint2Y->setText(QString::number(selectedPoints[1].y, 'f', 3)); + m_editPoint2Z->setText(QString::number(selectedPoints[1].z, 'f', 3)); } else { m_lblPoint2->setText("点2: --"); m_editPoint2X->setText("--"); m_editPoint2Y->setText("--"); m_editPoint2Z->setText("--"); } -} -void CloudViewMainWindow::onLineSelectModeChanged(bool checked) -{ - if (checked) { - // 纵向模式 - m_glWidget->setLineSelectMode(LineSelectMode::Vertical); - } else { - // 横向模式 - m_glWidget->setLineSelectMode(LineSelectMode::Horizontal); - } - m_lineNumberInput->setPlaceholderText("输入索引"); -} - -void CloudViewMainWindow::onClearLinePoints() -{ - m_glWidget->clearSelectedLine(); - m_glWidget->clearListHighlightPoint(); // 清除列表选中的高亮点 - m_lblLineIndex->setText("--"); - m_lblLinePointCount->setText("--"); - statusBar()->showMessage("已清除选线"); -} - -void CloudViewMainWindow::onLineSelected(const SelectedLineInfo& line) -{ - // 重新选线时清除列表高亮点 - m_glWidget->clearListHighlightPoint(); - - if (!line.valid) { - m_lblLineIndex->setText("--"); - m_lblLinePointCount->setText("--"); - return; - } - - // 状态栏显示:线号/索引号、线点数 - if (line.mode == LineSelectMode::Vertical) { - m_lblLineIndex->setText(QString::number(line.lineIndex)); - statusBar()->showMessage(QString("选中线 | 线号: %1 | 线点数: %2") - .arg(line.lineIndex) - .arg(line.pointCount)); - } else { - // 横向选线:显示索引号 - m_lblLineIndex->setText(QString::number(line.pointIndex)); - statusBar()->showMessage(QString("选中横向线 | 索引号: %1 | 线点数: %2") - .arg(line.pointIndex) - .arg(line.pointCount)); - } - m_lblLinePointCount->setText(QString::number(line.pointCount)); - - // 如果线上点对话框已打开,刷新内容 - updateLinePointsDialog(); -} - -void CloudViewMainWindow::onSelectLineByNumber() -{ - if (m_glWidget->getCloudCount() == 0) { - QMessageBox::warning(this, "提示", "请先加载点云"); - return; - } - - QString text = m_lineNumberInput->text().trimmed(); - if (text.isEmpty()) { - QMessageBox::warning(this, "提示", "请输入索引"); - return; - } - - bool ok; - int index = text.toInt(&ok); - if (!ok || index < 0) { - QMessageBox::warning(this, "提示", "请输入有效的索引(非负整数)"); - return; - } - - bool success = false; - if (m_rbVertical->isChecked()) { - // 纵向选线:直接使用索引 - success = m_glWidget->selectLineByIndex(index); - if (!success) { - QMessageBox::warning(this, "提示", QString("索引 %1 不存在").arg(index)); - } - } else { - // 横向选线:直接使用索引 - success = m_glWidget->selectHorizontalLineByIndex(index); - if (!success) { - QMessageBox::warning(this, "提示", QString("索引 %1 不存在").arg(index)); - } + if (selectedPoints.size() < 2 || !selectedPoints[0].valid || !selectedPoints[1].valid) { + clearMeasureResultDisplay(); } } - -QVector CloudViewMainWindow::getOriginalLinePoints(const SelectedLineInfo& lineInfo) -{ - QVector points; - - if (!lineInfo.valid || m_originalCloud.empty()) { - return points; - } - - if (lineInfo.mode == LineSelectMode::Vertical) { - // 纵向选线:获取同一条扫描线上的所有点 - for (size_t i = 0; i < m_originalCloud.points.size(); ++i) { - if (i < m_originalCloud.lineIndices.size() && - m_originalCloud.lineIndices[i] == lineInfo.lineIndex) { - const auto& pt = m_originalCloud.points[i]; - points.append(QVector3D(pt.x, pt.y, pt.z)); - } - } - } else { - // 横向选线:获取所有线的相同索引点 - if (m_currentLinePtNum > 0 && lineInfo.pointIndex >= 0) { - for (size_t i = 0; i < m_originalCloud.points.size(); ++i) { - int originalIdx = static_cast(i); - if (originalIdx % m_currentLinePtNum == lineInfo.pointIndex) { - const auto& pt = m_originalCloud.points[i]; - points.append(QVector3D(pt.x, pt.y, pt.z)); - } - } - } - } - - return points; -} - -void CloudViewMainWindow::updateLinePointsDialog() -{ - if (!m_linePointsDialog || !m_linePointsTable) { - return; - } - - SelectedLineInfo lineInfo = m_glWidget->getSelectedLine(); - if (!lineInfo.valid) { - m_linePointsTable->setRowCount(0); - m_linePointsDialog->setWindowTitle("线上点坐标"); - m_currentLinePoints.clear(); - return; - } - - // 从原始数据获取线上点(包含0,0,0) - m_currentLinePoints = getOriginalLinePoints(lineInfo); - - // 更新标题 - QString title; - if (lineInfo.mode == LineSelectMode::Vertical) { - title = QString("线上点坐标 - 线号: %1 (共 %2 个点)") - .arg(lineInfo.lineIndex) - .arg(m_currentLinePoints.size()); - } else { - title = QString("线上点坐标 - 索引号: %1 (共 %2 个点)") - .arg(lineInfo.pointIndex) - .arg(m_currentLinePoints.size()); - } - m_linePointsDialog->setWindowTitle(title); - - // 更新表格 - m_linePointsTable->setRowCount(m_currentLinePoints.size()); - - // 斑马线颜色 - QColor evenColor(245, 245, 245); // 浅灰色 - QColor oddColor(255, 255, 255); // 白色 - - for (int i = 0; i < m_currentLinePoints.size(); ++i) { - const QVector3D& pt = m_currentLinePoints[i]; - QColor rowColor = (i % 2 == 0) ? evenColor : oddColor; - - // 序号 - QTableWidgetItem* indexItem = new QTableWidgetItem(QString::number(i)); - indexItem->setTextAlignment(Qt::AlignCenter); - indexItem->setBackground(rowColor); - indexItem->setFlags(indexItem->flags() & ~Qt::ItemIsEditable); - m_linePointsTable->setItem(i, 0, indexItem); - - // X - QTableWidgetItem* xItem = new QTableWidgetItem(QString::number(pt.x(), 'f', 3)); - xItem->setTextAlignment(Qt::AlignCenter); - xItem->setBackground(rowColor); - xItem->setFlags(xItem->flags() & ~Qt::ItemIsEditable); - m_linePointsTable->setItem(i, 1, xItem); - - // Y - QTableWidgetItem* yItem = new QTableWidgetItem(QString::number(pt.y(), 'f', 3)); - yItem->setTextAlignment(Qt::AlignCenter); - yItem->setBackground(rowColor); - yItem->setFlags(yItem->flags() & ~Qt::ItemIsEditable); - m_linePointsTable->setItem(i, 2, yItem); - - // Z - QTableWidgetItem* zItem = new QTableWidgetItem(QString::number(pt.z(), 'f', 3)); - zItem->setTextAlignment(Qt::AlignCenter); - zItem->setBackground(rowColor); - zItem->setFlags(zItem->flags() & ~Qt::ItemIsEditable); - m_linePointsTable->setItem(i, 3, zItem); - } -} - -void CloudViewMainWindow::onShowLinePoints() -{ - SelectedLineInfo lineInfo = m_glWidget->getSelectedLine(); - if (!lineInfo.valid) { - QMessageBox::warning(this, "提示", "请先选择一条线"); - return; - } - - // 如果对话框已存在,刷新内容并显示 - if (m_linePointsDialog) { - updateLinePointsDialog(); - m_linePointsDialog->raise(); - m_linePointsDialog->activateWindow(); - return; - } - - // 创建对话框 - m_linePointsDialog = new QDialog(this); - m_linePointsDialog->resize(450, 500); - m_linePointsDialog->setAttribute(Qt::WA_DeleteOnClose); - - // 对话框关闭时清理指针 - connect(m_linePointsDialog, &QDialog::destroyed, this, [this]() { - m_linePointsDialog = nullptr; - m_linePointsTable = nullptr; - m_currentLinePoints.clear(); - m_glWidget->clearListHighlightPoint(); - }); - - QVBoxLayout* layout = new QVBoxLayout(m_linePointsDialog); - - // 提示标签 - QLabel* lblTip = new QLabel("点击行在3D视图中高亮显示", m_linePointsDialog); - lblTip->setStyleSheet("color: gray; font-size: 10px;"); - layout->addWidget(lblTip); - - // 创建表格控件 - m_linePointsTable = new QTableWidget(m_linePointsDialog); - m_linePointsTable->setColumnCount(4); - m_linePointsTable->setHorizontalHeaderLabels({"序号", "X", "Y", "Z"}); - m_linePointsTable->setFont(QFont("Consolas", 9)); - m_linePointsTable->setSelectionBehavior(QAbstractItemView::SelectRows); - m_linePointsTable->setSelectionMode(QAbstractItemView::SingleSelection); - m_linePointsTable->verticalHeader()->setVisible(false); - - // 设置列宽 - m_linePointsTable->setColumnWidth(0, 60); // 序号 - m_linePointsTable->setColumnWidth(1, 110); // X - m_linePointsTable->setColumnWidth(2, 110); // Y - m_linePointsTable->setColumnWidth(3, 110); // Z - - // 表头样式 - m_linePointsTable->horizontalHeader()->setStretchLastSection(true); - m_linePointsTable->horizontalHeader()->setDefaultAlignment(Qt::AlignCenter); - - connect(m_linePointsTable, &QTableWidget::cellClicked, - this, &CloudViewMainWindow::onLinePointTableClicked); - layout->addWidget(m_linePointsTable); - - // 关闭按钮 - QPushButton* btnClose = new QPushButton("关闭", m_linePointsDialog); - connect(btnClose, &QPushButton::clicked, m_linePointsDialog, &QDialog::close); - layout->addWidget(btnClose); - - // 填充数据 - updateLinePointsDialog(); - - m_linePointsDialog->show(); -} - -void CloudViewMainWindow::onLinePointTableClicked(int row, int column) -{ - Q_UNUSED(column); - - if (row >= 0 && row < m_currentLinePoints.size()) { - const QVector3D& pt = m_currentLinePoints[row]; - m_glWidget->setListHighlightPoint(pt); - - // 在状态栏显示选中点信息 - statusBar()->showMessage(QString("列表选中点 %1: (%2, %3, %4)") - .arg(row) - .arg(pt.x(), 0, 'f', 3) - .arg(pt.y(), 0, 'f', 3) - .arg(pt.z(), 0, 'f', 3)); - } -} - -void CloudViewMainWindow::onShowPose1() -{ - // 从输入框读取点1坐标 - bool ok = true; - float x1 = m_editPoint1X->text().toFloat(&ok); - if (!ok) { QMessageBox::warning(this, "错误", "点1 X 值无效"); return; } - float y1 = m_editPoint1Y->text().toFloat(&ok); - if (!ok) { QMessageBox::warning(this, "错误", "点1 Y 值无效"); return; } - float z1 = m_editPoint1Z->text().toFloat(&ok); - if (!ok) { QMessageBox::warning(this, "错误", "点1 Z 值无效"); return; } - - // 设置选中点(不存在则创建) - m_glWidget->setSelectedPointCoord(0, x1, y1, z1); - - // 读取姿态参数 - float rx = m_editRx1->text().toFloat(&ok); - if (!ok) { - QMessageBox::warning(this, "错误", "点1 RX 值无效"); - return; - } - - float ry = m_editRy1->text().toFloat(&ok); - if (!ok) { - QMessageBox::warning(this, "错误", "点1 RY 值无效"); - return; - } - - float rz = m_editRz1->text().toFloat(&ok); - if (!ok) { - QMessageBox::warning(this, "错误", "点1 RZ 值无效"); - return; - } - - // 读取坐标轴缩放 - float scale = m_editPoseScale->text().toFloat(&ok); - if (!ok || scale <= 0) scale = 25.0f; - - // 清除之前的姿态点 - m_glWidget->clearPosePoints(); - - // 创建点1的姿态点 - PosePoint pose1(x1, y1, z1, rx, ry, rz, scale); - QVector poses; - poses.append(pose1); - - // 如果点2输入框有值,也一起显示 - bool ok2 = true; - float x2 = m_editPoint2X->text().toFloat(&ok2); - float y2 = ok2 ? m_editPoint2Y->text().toFloat(&ok2) : 0; - float z2 = ok2 ? m_editPoint2Z->text().toFloat(&ok2) : 0; - if (ok2 && !(x2 == 0 && y2 == 0 && z2 == 0 && m_editPoint2X->text().isEmpty())) { - m_glWidget->setSelectedPointCoord(1, x2, y2, z2); - - bool okR = true; - float rx2 = m_editRx2->text().toFloat(&okR); - float ry2 = okR ? m_editRy2->text().toFloat(&okR) : 0; - float rz2 = okR ? m_editRz2->text().toFloat(&okR) : 0; - if (okR) { - PosePoint pose2(x2, y2, z2, rx2, ry2, rz2, scale); - poses.append(pose2); - } - } - - // 添加到显示 - m_glWidget->addPosePoints(poses); - - statusBar()->showMessage(QString("已显示点1姿态 (%1, %2, %3) 旋转(%4°, %5°, %6°)") - .arg(x1, 0, 'f', 3).arg(y1, 0, 'f', 3).arg(z1, 0, 'f', 3) - .arg(rx, 0, 'f', 1).arg(ry, 0, 'f', 1).arg(rz, 0, 'f', 1)); - - LOG_INFO("[CloudView] Show pose1 at (%.3f, %.3f, %.3f) with rotation (%.1f, %.1f, %.1f)\n", - x1, y1, z1, rx, ry, rz); -} - -void CloudViewMainWindow::onShowPose2() -{ - // 从输入框读取点2坐标 - bool ok = true; - float x2 = m_editPoint2X->text().toFloat(&ok); - if (!ok) { QMessageBox::warning(this, "错误", "点2 X 值无效"); return; } - float y2 = m_editPoint2Y->text().toFloat(&ok); - if (!ok) { QMessageBox::warning(this, "错误", "点2 Y 值无效"); return; } - float z2 = m_editPoint2Z->text().toFloat(&ok); - if (!ok) { QMessageBox::warning(this, "错误", "点2 Z 值无效"); return; } - - // 设置选中点(不存在则创建) - m_glWidget->setSelectedPointCoord(1, x2, y2, z2); - - // 读取姿态参数 - float rx = m_editRx2->text().toFloat(&ok); - if (!ok) { - QMessageBox::warning(this, "错误", "点2 RX 值无效"); - return; - } - - float ry = m_editRy2->text().toFloat(&ok); - if (!ok) { - QMessageBox::warning(this, "错误", "点2 RY 值无效"); - return; - } - - float rz = m_editRz2->text().toFloat(&ok); - if (!ok) { - QMessageBox::warning(this, "错误", "点2 RZ 值无效"); - return; - } - - // 读取坐标轴缩放 - float scale = m_editPoseScale->text().toFloat(&ok); - if (!ok || scale <= 0) scale = 25.0f; - - // 清除之前的姿态点 - m_glWidget->clearPosePoints(); - - QVector poses; - - // 如果点1输入框有值,也一起显示 - bool ok1 = true; - float x1 = m_editPoint1X->text().toFloat(&ok1); - float y1 = ok1 ? m_editPoint1Y->text().toFloat(&ok1) : 0; - float z1 = ok1 ? m_editPoint1Z->text().toFloat(&ok1) : 0; - if (ok1 && !(x1 == 0 && y1 == 0 && z1 == 0 && m_editPoint1X->text().isEmpty())) { - m_glWidget->setSelectedPointCoord(0, x1, y1, z1); - - bool okR = true; - float rx1 = m_editRx1->text().toFloat(&okR); - float ry1 = okR ? m_editRy1->text().toFloat(&okR) : 0; - float rz1 = okR ? m_editRz1->text().toFloat(&okR) : 0; - if (okR) { - PosePoint pose1(x1, y1, z1, rx1, ry1, rz1, scale); - poses.append(pose1); - } - } - - // 创建点2的姿态点 - PosePoint pose2(x2, y2, z2, rx, ry, rz, scale); - poses.append(pose2); - - // 添加到显示 - m_glWidget->addPosePoints(poses); - - statusBar()->showMessage(QString("已显示点2姿态 (%1, %2, %3) 旋转(%4°, %5°, %6°)") - .arg(x2, 0, 'f', 3).arg(y2, 0, 'f', 3).arg(z2, 0, 'f', 3) - .arg(rx, 0, 'f', 1).arg(ry, 0, 'f', 1).arg(rz, 0, 'f', 1)); - - LOG_INFO("[CloudView] Show pose2 at (%.3f, %.3f, %.3f) with rotation (%.1f, %.1f, %.1f)\n", - x2, y2, z2, rx, ry, rz); -} - -void CloudViewMainWindow::onShowInputLine() -{ - // 读取点1坐标 - bool ok = true; - float x1 = m_editLineX1->text().toFloat(&ok); - if (!ok) { - QMessageBox::warning(this, "错误", "点1 X 值无效"); - return; - } - - float y1 = m_editLineY1->text().toFloat(&ok); - if (!ok) { - QMessageBox::warning(this, "错误", "点1 Y 值无效"); - return; - } - - float z1 = m_editLineZ1->text().toFloat(&ok); - if (!ok) { - QMessageBox::warning(this, "错误", "点1 Z 值无效"); - return; - } - - // 读取点2坐标 - float x2 = m_editLineX2->text().toFloat(&ok); - if (!ok) { - QMessageBox::warning(this, "错误", "点2 X 值无效"); - return; - } - - float y2 = m_editLineY2->text().toFloat(&ok); - if (!ok) { - QMessageBox::warning(this, "错误", "点2 Y 值无效"); - return; - } - - float z2 = m_editLineZ2->text().toFloat(&ok); - if (!ok) { - QMessageBox::warning(this, "错误", "点2 Z 值无效"); - return; - } - - // 清除之前的线段 - m_glWidget->clearLineSegments(); - - // 创建线段(红色) - LineSegment segment(x1, y1, z1, x2, y2, z2, 1.0f, 0.0f, 0.0f); - QVector segments; - segments.append(segment); - - // 添加到显示 - m_glWidget->addLineSegments(segments); - - // 计算距离 - float dx = x2 - x1; - float dy = y2 - y1; - float dz = z2 - z1; - float distance = std::sqrt(dx * dx + dy * dy + dz * dz); - - statusBar()->showMessage(QString("已显示线段 (%1,%2,%3) → (%4,%5,%6) 长度: %7") - .arg(x1, 0, 'f', 1).arg(y1, 0, 'f', 1).arg(z1, 0, 'f', 1) - .arg(x2, 0, 'f', 1).arg(y2, 0, 'f', 1).arg(z2, 0, 'f', 1) - .arg(distance, 0, 'f', 3)); - - LOG_INFO("[CloudView] Show input line from (%.3f, %.3f, %.3f) to (%.3f, %.3f, %.3f) length=%.3f\n", - x1, y1, z1, x2, y2, z2, distance); -} - -void CloudViewMainWindow::onClearInputLine() -{ - m_glWidget->clearLineSegments(); - statusBar()->showMessage(QString("已清除输入的线段")); -} - -void CloudViewMainWindow::onEyeToHandTransform() -{ - if (m_glWidget->getCloudCount() == 0) { - QMessageBox::warning(this, "提示", "请先加载点云"); - return; - } - - QDialog dialog(this); - dialog.setWindowTitle("眼在手外 - 矩阵变换"); - dialog.setModal(true); - - QVBoxLayout* layout = new QVBoxLayout(&dialog); - - QLabel* tip = new QLabel("输入或从文件加载 4x4 变换矩阵(相机坐标系 -> 基坐标系),直接应用到所有点云。", &dialog); - tip->setWordWrap(true); - tip->setStyleSheet("color: gray; font-size: 10px;"); - layout->addWidget(tip); - - QLabel* lblMatrix = new QLabel("变换矩阵 (4x4):", &dialog); - lblMatrix->setStyleSheet("font-weight: bold;"); - layout->addWidget(lblMatrix); - - QTextEdit* matrixEdit = new QTextEdit(&dialog); - matrixEdit->setFont(QFont("Consolas", 10)); - matrixEdit->setMinimumHeight(100); - matrixEdit->setMaximumHeight(120); - matrixEdit->setPlainText( - "1.0 0.0 0.0 0.0\n" - "0.0 1.0 0.0 0.0\n" - "0.0 0.0 1.0 0.0\n" - "0.0 0.0 0.0 1.0"); - layout->addWidget(matrixEdit); - - QPushButton* btnLoad = new QPushButton("从文件加载矩阵", &dialog); - connect(btnLoad, &QPushButton::clicked, [this, matrixEdit]() { - const QString fileName = QFileDialog::getOpenFileName( - this, "打开矩阵文件", QString(), - "标定矩阵文件 (*.ini *.txt);;INI文件 (*.ini);;文本文件 (*.txt);;所有文件 (*.*)"); - if (fileName.isEmpty()) return; - - if (fileName.endsWith(".ini", Qt::CaseInsensitive)) { - QVector> matrixList; - QString errorMessage; - if (!parseCalibIniFile(fileName, matrixList, &errorMessage)) { - QMessageBox::warning(this, "格式错误", errorMessage); - return; - } - if (matrixList.size() == 1) { - matrixEdit->setPlainText(formatMatrixText(matrixList[0].second)); - return; - } - QDialog dlg(this); - dlg.setWindowTitle("选择标定矩阵"); - QVBoxLayout* l = new QVBoxLayout(&dlg); - l->addWidget(new QLabel(QString("文件中包含 %1 个标定矩阵,请选择:").arg(matrixList.size()), &dlg)); - QListWidget* lw = new QListWidget(&dlg); - for (auto& m : matrixList) lw->addItem(m.first); - lw->setCurrentRow(0); - l->addWidget(lw); - QDialogButtonBox* bb = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dlg); - l->addWidget(bb); - connect(bb, &QDialogButtonBox::accepted, &dlg, &QDialog::accept); - connect(bb, &QDialogButtonBox::rejected, &dlg, &QDialog::reject); - if (dlg.exec() == QDialog::Accepted && lw->currentRow() >= 0 && lw->currentRow() < matrixList.size()) - matrixEdit->setPlainText(formatMatrixText(matrixList[lw->currentRow()].second)); - return; - } - - QFile file(fileName); - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - QMessageBox::critical(this, "错误", QString("无法打开文件: %1").arg(fileName)); - return; - } - QTextStream in(&file); - QString text = in.readAll(); - file.close(); - QMatrix4x4 matrix; - QString errorMessage; - if (!parseMatrixText(text, matrix, &errorMessage)) { - QMessageBox::warning(this, "格式错误", errorMessage); - return; - } - matrixEdit->setPlainText(formatMatrixText(matrix)); - }); - layout->addWidget(btnLoad); - - QLabel* lblFormat = new QLabel("矩阵文件格式:4 行,每行 4 个数值,分隔符:空格 / Tab / 逗号", &dialog); - lblFormat->setWordWrap(true); - lblFormat->setStyleSheet("color: gray; font-size: 9px;"); - layout->addWidget(lblFormat); - - QDialogButtonBox* btnBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); - btnBox->button(QDialogButtonBox::Ok)->setText("应用变换"); - btnBox->button(QDialogButtonBox::Ok)->setStyleSheet("QPushButton { background-color: #4CAF50; color: white; font-weight: bold; }" - "QPushButton:hover { background-color: #45a049; }"); - layout->addWidget(btnBox); - connect(btnBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); - connect(btnBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); - - if (dialog.exec() != QDialog::Accepted) return; - - QMatrix4x4 matrix; - QString errorMessage; - if (!parseMatrixText(matrixEdit->toPlainText().trimmed(), matrix, &errorMessage)) { - QMessageBox::warning(this, "格式错误", errorMessage); - return; - } - if (matrix.isIdentity()) { - QMessageBox::information(this, "提示", "当前矩阵为单位矩阵,无需变换"); - return; - } - - applyTransformToAllClouds(matrix); - statusBar()->showMessage("已将眼在手外矩阵变换应用到所有点云"); - LOG_INFO("[CloudView] Applied eye-to-hand matrix transform to all point clouds\n"); -} - -void CloudViewMainWindow::onEyeInHandTransform() -{ - if (m_glWidget->getCloudCount() == 0) { - QMessageBox::warning(this, "提示", "请先加载点云"); - return; - } - - QDialog dialog(this); - dialog.setWindowTitle("眼在手上 - 矩阵变换"); - dialog.setModal(true); - - QVBoxLayout* layout = new QVBoxLayout(&dialog); - - QLabel* tip = new QLabel( - "使用 4x4 矩阵作为“相机坐标系 -> 末端坐标系”的手眼矩阵。
" - "输入当前机械臂位姿(单位:mm / deg)后,将所有点云转换到机械臂基坐标系。
" - "角度组合方式:ZYX(Rz-Ry-Rx)。", &dialog); - tip->setWordWrap(true); - tip->setTextFormat(Qt::RichText); - tip->setStyleSheet("color: gray; font-size: 10px;"); - layout->addWidget(tip); - - QLabel* lblMatrix = new QLabel("手眼矩阵 (4x4):", &dialog); - lblMatrix->setStyleSheet("font-weight: bold;"); - layout->addWidget(lblMatrix); - - QTextEdit* matrixEdit = new QTextEdit(&dialog); - matrixEdit->setFont(QFont("Consolas", 10)); - matrixEdit->setMinimumHeight(100); - matrixEdit->setMaximumHeight(120); - matrixEdit->setPlainText( - "1.0 0.0 0.0 0.0\n" - "0.0 1.0 0.0 0.0\n" - "0.0 0.0 1.0 0.0\n" - "0.0 0.0 0.0 1.0"); - layout->addWidget(matrixEdit); - - QPushButton* btnLoad = new QPushButton("从文件加载矩阵", &dialog); - connect(btnLoad, &QPushButton::clicked, [this, matrixEdit]() { - const QString fileName = QFileDialog::getOpenFileName( - this, "打开矩阵文件", QString(), - "标定矩阵文件 (*.ini *.txt);;INI文件 (*.ini);;文本文件 (*.txt);;所有文件 (*.*)"); - if (fileName.isEmpty()) return; - - if (fileName.endsWith(".ini", Qt::CaseInsensitive)) { - QVector> matrixList; - QString errorMessage; - if (!parseCalibIniFile(fileName, matrixList, &errorMessage)) { - QMessageBox::warning(this, "格式错误", errorMessage); - return; - } - if (matrixList.size() == 1) { - matrixEdit->setPlainText(formatMatrixText(matrixList[0].second)); - return; - } - QDialog dlg(this); - dlg.setWindowTitle("选择标定矩阵"); - QVBoxLayout* l = new QVBoxLayout(&dlg); - l->addWidget(new QLabel(QString("文件中包含 %1 个标定矩阵,请选择:").arg(matrixList.size()), &dlg)); - QListWidget* lw = new QListWidget(&dlg); - for (auto& m : matrixList) lw->addItem(m.first); - lw->setCurrentRow(0); - l->addWidget(lw); - QDialogButtonBox* bb = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dlg); - l->addWidget(bb); - connect(bb, &QDialogButtonBox::accepted, &dlg, &QDialog::accept); - connect(bb, &QDialogButtonBox::rejected, &dlg, &QDialog::reject); - if (dlg.exec() == QDialog::Accepted && lw->currentRow() >= 0 && lw->currentRow() < matrixList.size()) - matrixEdit->setPlainText(formatMatrixText(matrixList[lw->currentRow()].second)); - return; - } - - QFile file(fileName); - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - QMessageBox::critical(this, "错误", QString("无法打开文件: %1").arg(fileName)); - return; - } - QTextStream in(&file); - QString text = in.readAll(); - file.close(); - QMatrix4x4 matrix; - QString errorMessage; - if (!parseMatrixText(text, matrix, &errorMessage)) { - QMessageBox::warning(this, "格式错误", errorMessage); - return; - } - matrixEdit->setPlainText(formatMatrixText(matrix)); - }); - layout->addWidget(btnLoad); - - // 机械臂位姿输入 - QGroupBox* poseGroup = new QGroupBox("机械臂位姿", &dialog); - QGridLayout* poseLayout = new QGridLayout(poseGroup); - poseLayout->setHorizontalSpacing(8); - poseLayout->setVerticalSpacing(6); - - auto addPoseEdit = [&](const QString& label, int row, int col) -> QLineEdit* { - QLabel* lbl = new QLabel(label, poseGroup); - QLineEdit* edit = new QLineEdit("0.0", poseGroup); - edit->setAlignment(Qt::AlignRight); - edit->setFont(QFont("Consolas", 10)); - poseLayout->addWidget(lbl, row, col * 2); - poseLayout->addWidget(edit, row, col * 2 + 1); - return edit; - }; - - QLineEdit* editX = addPoseEdit("X:", 0, 0); - QLineEdit* editY = addPoseEdit("Y:", 0, 1); - QLineEdit* editZ = addPoseEdit("Z:", 0, 2); - QLineEdit* editRx = addPoseEdit("RX:", 1, 0); - QLineEdit* editRy = addPoseEdit("RY:", 1, 1); - QLineEdit* editRz = addPoseEdit("RZ:", 1, 2); - layout->addWidget(poseGroup); - - QDialogButtonBox* btnBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); - btnBox->button(QDialogButtonBox::Ok)->setText("应用转换"); - btnBox->button(QDialogButtonBox::Ok)->setStyleSheet("QPushButton { background-color: #1976D2; color: white; font-weight: bold; }" - "QPushButton:hover { background-color: #1565C0; }"); - layout->addWidget(btnBox); - connect(btnBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); - connect(btnBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); - - if (dialog.exec() != QDialog::Accepted) return; - - QMatrix4x4 handEyeMatrix; - QString errorMessage; - if (!parseMatrixText(matrixEdit->toPlainText().trimmed(), handEyeMatrix, &errorMessage)) { - QMessageBox::warning(this, "格式错误", QString("手眼矩阵无效:%1").arg(errorMessage)); - return; - } - - bool ok = false; - float vals[6]; - const char* labels[] = {"X", "Y", "Z", "RX", "RY", "RZ"}; - QLineEdit* edits[] = {editX, editY, editZ, editRx, editRy, editRz}; - for (int i = 0; i < 6; ++i) { - vals[i] = edits[i]->text().trimmed().toFloat(&ok); - if (!ok) { - QMessageBox::warning(this, "格式错误", QString("机械臂 %1 位姿无效").arg(labels[i])); - return; - } - } - - const QMatrix4x4 totalMatrix = buildRobotPoseMatrix(vals[0], vals[1], vals[2], vals[3], vals[4], vals[5]) * handEyeMatrix; - if (totalMatrix.isIdentity()) { - QMessageBox::information(this, "提示", "当前位姿和手眼矩阵组合为单位矩阵,无需变换"); - return; - } - - applyTransformToAllClouds(totalMatrix); - statusBar()->showMessage( - QString("已应用眼在手上转换,机械臂位姿: [%1, %2, %3, %4, %5, %6]") - .arg(vals[0], 0, 'f', 3).arg(vals[1], 0, 'f', 3).arg(vals[2], 0, 'f', 3) - .arg(vals[3], 0, 'f', 3).arg(vals[4], 0, 'f', 3).arg(vals[5], 0, 'f', 3)); - LOG_INFO("[CloudView] Applied eye-in-hand transform, robot pose=(%.3f, %.3f, %.3f, %.3f, %.3f, %.3f)\n", - vals[0], vals[1], vals[2], vals[3], vals[4], vals[5]); -} - -void CloudViewMainWindow::onSavePointCloud() -{ - if (m_glWidget->getCloudCount() == 0) { - QMessageBox::warning(this, "提示", "请先加载点云"); - return; - } - - const QString fileName = QFileDialog::getSaveFileName( - this, "保存 TXT 点云", QString(), "文本文件 (*.txt);;所有文件 (*.*)"); - if (fileName.isEmpty()) return; - - std::vector> scanLines; - const size_t totalPoints = m_glWidget->getAllCloudsByLines(scanLines); - if (scanLines.empty() || totalPoints == 0) { - QMessageBox::warning(this, "提示", "当前无可保存的点云数据"); - return; - } - - // 进度对话框 - QProgressDialog progress("正在保存点云...", "取消", 0, 100, this); - progress.setWindowTitle("保存点云"); - progress.setWindowModality(Qt::WindowModal); - progress.setMinimumDuration(0); - progress.setAutoClose(true); - progress.setAutoReset(true); - - // 回调实现 - class SaveProgressCallback : public ISaveProgressCallback - { - public: - SaveProgressCallback(QProgressDialog& dlg) : m_dlg(dlg) {} - void onSaveProgress(float progress) override - { - m_dlg.setValue(static_cast(progress * 100)); - QCoreApplication::processEvents(); - } - bool isSaveCancelled() const override - { - return m_dlg.wasCanceled(); - } - private: - QProgressDialog& m_dlg; - }; - - SaveProgressCallback callback(progress); - - LaserDataLoader loader; - const int ret = loader.DebugSaveLaser(fileName.toStdString(), scanLines, &callback); - progress.setValue(100); - - if (ret != 0) { - if (callback.isSaveCancelled()) { - statusBar()->showMessage("保存已取消"); - } else { - QMessageBox::critical(this, "保存失败", - QString("保存点云失败: %1").arg(QString::fromStdString(loader.GetLastError()))); - } - return; - } - - statusBar()->showMessage( - QString("已保存点云: %1 条线, %2 个点 -> %3") - .arg(scanLines.size()).arg(totalPoints).arg(QFileInfo(fileName).fileName())); - LOG_INFO("[CloudView] Saved %zu lines, %zu points to %s\n", - scanLines.size(), totalPoints, fileName.toStdString().c_str()); -} - -void CloudViewMainWindow::onSavePlyPcdCloud() -{ - if (m_glWidget->getCloudCount() == 0) { - QMessageBox::warning(this, "提示", "请先加载点云"); - return; - } - - const QString fileName = QFileDialog::getSaveFileName( - this, "保存 PLY/PCD 点云", QString(), - "PLY 文件 (*.ply);;PCD 文件 (*.pcd);;所有文件 (*.*)"); - if (fileName.isEmpty()) return; - - // 获取所有显示的点云,展平为 PointCloudXYZ(跳过零点) - std::vector> scanLines; - const size_t totalPoints = m_glWidget->getAllCloudsByLines(scanLines); - if (scanLines.empty() || totalPoints == 0) { - QMessageBox::warning(this, "提示", "当前无可保存的点云数据"); - return; - } - - PointCloudXYZ cloud; - const float EPS = 1e-6f; - for (const auto& line : scanLines) { - for (const auto& pt : line) { - // 跳过零点(网格填充的空位) - if (std::fabs(pt.pt3D.x) < EPS && - std::fabs(pt.pt3D.y) < EPS && - std::fabs(pt.pt3D.z) < EPS) - continue; - cloud.points.emplace_back(pt.pt3D.x, pt.pt3D.y, pt.pt3D.z); - } - } - - if (cloud.empty()) { - QMessageBox::warning(this, "提示", "当前无可保存的有效点云数据"); - return; - } - - // 根据扩展名选择保存格式 - const std::string path = fileName.toStdString(); - std::string ext; - const auto dotPos = path.rfind('.'); - if (dotPos != std::string::npos) - ext = path.substr(dotPos + 1); - for (auto& c : ext) c = static_cast(std::tolower(c)); - - int ret = -1; - if (ext == "ply") { - ret = m_converter->saveToPly(path, cloud); - } else if (ext == "pcd") { - ret = m_converter->saveToPcd(path, cloud); - } else { - // 用户手动输入了其他扩展名,默认用 PLY - ret = m_converter->saveToPly(path, cloud); - } - - if (ret != 0) { - QMessageBox::critical(this, "保存失败", - QString("保存点云失败: %1") - .arg(QString::fromStdString(m_converter->getLastError()))); - return; - } - - statusBar()->showMessage( - QString("已保存点云: %1 个点 -> %2") - .arg(cloud.points.size()).arg(QFileInfo(fileName).fileName())); - LOG_INFO("[CloudView] Saved %zu points to %s\n", - cloud.points.size(), fileName.toStdString().c_str()); -} - -void CloudViewMainWindow::onBatchConvertTxtToPly() -{ - // 选择源文件夹 - const QString srcDir = QFileDialog::getExistingDirectory( - this, "选择源文件夹(包含 txt 点云文件)", QString(), - QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); - if (srcDir.isEmpty()) return; - - // 选择目标文件夹 - const QString dstDir = QFileDialog::getExistingDirectory( - this, "选择目标文件夹(PLY 文件输出位置)", srcDir, - QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); - if (dstDir.isEmpty()) return; - - // 递归收集所有 .txt 文件及其相对路径 - struct FileEntry - { - QString absolutePath; - QString relativePath; // 相对于源文件夹的路径,用于构建目标路径 - }; - QVector fileEntries; - - { - QDirIterator it(srcDir, QStringList("*.txt"), QDir::Files, - QDirIterator::Subdirectories | QDirIterator::FollowSymlinks); - while (it.hasNext()) { - it.next(); - FileEntry entry; - entry.absolutePath = it.filePath(); - // 计算相对路径(文件路径去掉源目录前缀) - entry.relativePath = QDir(srcDir).relativeFilePath(it.filePath()); - fileEntries.append(entry); - } - } - - if (fileEntries.isEmpty()) { - QMessageBox::information(this, "提示", - QString("源文件夹及其子目录中没有找到 .txt 文件:\n%1").arg(srcDir)); - return; - } - - // 按相对路径排序 - std::sort(fileEntries.begin(), fileEntries.end(), - [](const FileEntry& a, const FileEntry& b) { - return a.relativePath < b.relativePath; - }); - - // 进度对话框 - QProgressDialog progress("正在批量转换 txt -> ply...", "取消", 0, fileEntries.size(), this); - progress.setWindowTitle("批量转换"); - progress.setWindowModality(Qt::WindowModal); - progress.setMinimumDuration(0); - progress.setAutoClose(true); - - // 使用独立的转换器,避免干扰主窗口状态 - PointCloudConverter converter; - int successCount = 0; - int failCount = 0; - int skipCount = 0; - QStringList failedFiles; - - const float kZeroEps = 1e-6f; - - for (int i = 0; i < fileEntries.size(); ++i) { - if (progress.wasCanceled()) { - statusBar()->showMessage("批量转换已取消"); - break; - } - - const FileEntry& entry = fileEntries[i]; - const QString& srcPath = entry.absolutePath; - const QString& relPath = entry.relativePath; - - // 构建目标路径:目标文件夹 + 相对路径,扩展名改为 .ply - const QString baseName = QFileInfo(relPath).completeBaseName(); - const QString relDir = QFileInfo(relPath).path(); // 相对路径中的目录部分 - const QString dstRelPath = (relDir == "." ? baseName + ".ply" - : relDir + "/" + baseName + ".ply"); - const QString dstPath = dstDir + "/" + dstRelPath; - - // 确保目标子目录存在 - const QString dstParentDir = QFileInfo(dstPath).absolutePath(); - if (!QDir().mkpath(dstParentDir)) { - failCount++; - failedFiles.append(QString("%1 (无法创建目标目录)").arg(relPath)); - LOG_WARN("[CloudView] Batch convert: failed to create dir %s\n", - dstParentDir.toStdString().c_str()); - continue; - } - - // 显示简短的相对路径便于阅读 - const QString displayPath = relPath.length() > 60 - ? ("..." + relPath.right(57)) : relPath; - - progress.setLabelText(QString("正在转换: %1 (%2/%3)") - .arg(displayPath).arg(i + 1).arg(fileEntries.size())); - progress.setValue(i); - QCoreApplication::processEvents(); - - // 加载 txt 点云 - PointCloudXYZ cloud; - int loadRet = converter.loadFromTxt(srcPath.toStdString(), cloud); - if (loadRet != 0) { - failCount++; - failedFiles.append(QString("%1 (加载失败: %2)") - .arg(relPath) - .arg(QString::fromStdString(converter.getLastError()))); - LOG_WARN("[CloudView] Batch convert: failed to load %s\n", - srcPath.toStdString().c_str()); - continue; - } - - // 过滤掉零点,构建用于保存的点云 - PointCloudXYZ filteredCloud; - filteredCloud.reserve(cloud.size()); - for (const auto& pt : cloud.points) { - if (std::fabs(pt.x) < kZeroEps && - std::fabs(pt.y) < kZeroEps && - std::fabs(pt.z) < kZeroEps) - continue; - filteredCloud.push_back(pt); - } - - if (filteredCloud.empty()) { - failCount++; - failedFiles.append(QString("%1 (无有效点)").arg(relPath)); - LOG_WARN("[CloudView] Batch convert: no valid points in %s\n", - srcPath.toStdString().c_str()); - continue; - } - - // 保存为 PLY - int saveRet = converter.saveToPly(dstPath.toStdString(), filteredCloud); - if (saveRet != 0) { - failCount++; - failedFiles.append(QString("%1 (保存失败: %2)") - .arg(relPath) - .arg(QString::fromStdString(converter.getLastError()))); - LOG_WARN("[CloudView] Batch convert: failed to save %s\n", - dstPath.toStdString().c_str()); - continue; - } - - successCount++; - LOG_INFO("[CloudView] Batch convert: %s -> %s (%zu points)\n", - srcPath.toStdString().c_str(), - dstPath.toStdString().c_str(), - filteredCloud.size()); - } - - progress.setValue(fileEntries.size()); - - // 汇总结果 - const int totalCount = fileEntries.size(); - QString summary = QString("批量转换完成!\n\n" - "总文件数: %1\n" - "成功: %2\n" - "失败: %3") - .arg(totalCount) - .arg(successCount) - .arg(failCount); - if (skipCount > 0) { - summary += QString("\n跳过: %1").arg(skipCount); - } - - if (!failedFiles.isEmpty()) { - summary += "\n\n失败文件:\n"; - // 最多显示前 10 个失败文件 - const int maxShow = std::min(failedFiles.size(), 10); - for (int i = 0; i < maxShow; ++i) { - summary += QString(" • %1\n").arg(failedFiles[i]); - } - if (failedFiles.size() > maxShow) { - summary += QString(" ... 还有 %1 个\n") - .arg(failedFiles.size() - maxShow); - } - } - - QMessageBox::information(this, "转换结果", summary); - statusBar()->showMessage( - QString("批量转换完成: %1 成功, %2 失败").arg(successCount).arg(failCount)); -} - -void CloudViewMainWindow::onRotateCloudByZ() -{ - if (m_glWidget->getCloudCount() == 0) { - QMessageBox::warning(this, "提示", "请先加载点云"); - return; - } - - QDialog dialog(this); - dialog.setWindowTitle("点云旋转"); - dialog.setModal(true); - - QVBoxLayout* layout = new QVBoxLayout(&dialog); - - QLabel* tip = new QLabel("对 Z 值小于阈值的点绕指定轴旋转指定角度。", &dialog); - tip->setWordWrap(true); - tip->setStyleSheet("color: gray;"); - layout->addWidget(tip); - - auto makeSpinBox = [&](double minValue, double maxValue, double value, double step = 1.0) { - QDoubleSpinBox* spin = new QDoubleSpinBox(&dialog); - spin->setDecimals(3); - spin->setRange(minValue, maxValue); - spin->setValue(value); - spin->setSingleStep(step); - spin->setAlignment(Qt::AlignRight); - return spin; - }; - - QGroupBox* thresholdGroup = new QGroupBox("Z 轴阈值", &dialog); - QFormLayout* thresholdLayout = new QFormLayout(thresholdGroup); - QDoubleSpinBox* zThreshold = makeSpinBox(-100000.0, 100000.0, 0.0, 10.0); - thresholdLayout->addRow("Z < ", zThreshold); - layout->addWidget(thresholdGroup); - - QGroupBox* rotateGroup = new QGroupBox("旋转角度 (度)", &dialog); - QFormLayout* rotateLayout = new QFormLayout(rotateGroup); - QDoubleSpinBox* rotX = makeSpinBox(-360.0, 360.0, 0.0, 5.0); - QDoubleSpinBox* rotY = makeSpinBox(-360.0, 360.0, 0.0, 5.0); - QDoubleSpinBox* rotZ = makeSpinBox(-360.0, 360.0, 0.0, 5.0); - rotateLayout->addRow("绕 X 轴:", rotX); - rotateLayout->addRow("绕 Y 轴:", rotY); - rotateLayout->addRow("绕 Z 轴:", rotZ); - layout->addWidget(rotateGroup); - - QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); - connect(buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); - connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); - layout->addWidget(buttonBox); - - if (dialog.exec() != QDialog::Accepted) { - return; - } - - // 构建旋转矩阵: Rz * Ry * Rx - const QMatrix4x4 rotMatrix = - makeAxisRotationMatrix('z', rotZ->value() * kDegToRad) * - makeAxisRotationMatrix('y', rotY->value() * kDegToRad) * - makeAxisRotationMatrix('x', rotX->value() * kDegToRad); - - const size_t count = m_glWidget->rotateCloudsByZThreshold(rotMatrix, static_cast(zThreshold->value())); - if (count == 0) { - QMessageBox::information(this, "提示", "没有满足条件的点 (Z < 阈值)"); - return; - } - - statusBar()->showMessage( - QString("已旋转 %1 个点 (Z < %2, 绕X %3° Y %4° Z %5°)") - .arg(count) - .arg(zThreshold->value(), 0, 'f', 3) - .arg(rotX->value(), 0, 'f', 1) - .arg(rotY->value(), 0, 'f', 1) - .arg(rotZ->value(), 0, 'f', 1)); - LOG_INFO("[CloudView] Rotated %zu points with Z < %.3f by (%.1f, %.1f, %.1f) deg\n", - count, zThreshold->value(), rotX->value(), rotY->value(), rotZ->value()); -} - -void CloudViewMainWindow::onGeneratePointCloud() -{ - QDialog dialog(this); - dialog.setWindowTitle("生成长方体点云"); - dialog.setModal(true); - - QVBoxLayout* layout = new QVBoxLayout(&dialog); - - QLabel* tip = new QLabel("根据中心点和 XYZ 尺寸生成轴对齐长方体表面点云。", &dialog); - tip->setWordWrap(true); - tip->setStyleSheet("color: gray;"); - layout->addWidget(tip); - - auto makeSpinBox = [&](double minValue, double maxValue, double value) { - QDoubleSpinBox* spin = new QDoubleSpinBox(&dialog); - spin->setDecimals(3); - spin->setRange(minValue, maxValue); - spin->setValue(value); - spin->setSingleStep(1.0); - spin->setAlignment(Qt::AlignRight); - return spin; - }; - - QGroupBox* centerGroup = new QGroupBox("中心点", &dialog); - QFormLayout* centerLayout = new QFormLayout(centerGroup); - QDoubleSpinBox* centerX = makeSpinBox(-100000.0, 100000.0, 0.0); - QDoubleSpinBox* centerY = makeSpinBox(-100000.0, 100000.0, 0.0); - QDoubleSpinBox* centerZ = makeSpinBox(-100000.0, 100000.0, 0.0); - centerLayout->addRow("X:", centerX); - centerLayout->addRow("Y:", centerY); - centerLayout->addRow("Z:", centerZ); - layout->addWidget(centerGroup); - - QGroupBox* sizeGroup = new QGroupBox("三轴方向大小", &dialog); - QFormLayout* sizeLayout = new QFormLayout(sizeGroup); - QDoubleSpinBox* sizeX = makeSpinBox(0.001, 100000.0, 100.0); - QDoubleSpinBox* sizeY = makeSpinBox(0.001, 100000.0, 100.0); - QDoubleSpinBox* sizeZ = makeSpinBox(0.001, 100000.0, 100.0); - QDoubleSpinBox* spacing = makeSpinBox(0.001, 10000.0, 5.0); - sizeLayout->addRow("X 尺寸:", sizeX); - sizeLayout->addRow("Y 尺寸:", sizeY); - sizeLayout->addRow("Z 尺寸:", sizeZ); - sizeLayout->addRow("点间距:", spacing); - layout->addWidget(sizeGroup); - - QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); - connect(buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); - connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); - layout->addWidget(buttonBox); - - if (dialog.exec() != QDialog::Accepted) { - return; - } - - const QVector3D center(static_cast(centerX->value()), - static_cast(centerY->value()), - static_cast(centerZ->value())); - const QVector3D size(static_cast(sizeX->value()), - static_cast(sizeY->value()), - static_cast(sizeZ->value())); - const float pointSpacing = static_cast(spacing->value()); - - const int gridX = std::max(1, static_cast(std::ceil(size.x() / pointSpacing))) + 1; - const int gridY = std::max(1, static_cast(std::ceil(size.y() / pointSpacing))) + 1; - const int gridZ = std::max(1, static_cast(std::ceil(size.z() / pointSpacing))) + 1; - const double estimatedGridCount = static_cast(gridX) * gridY * gridZ; - if (estimatedGridCount > 3000000.0) { - QMessageBox::warning(this, "提示", "点数预计过大,请增大点间距或减小尺寸。"); - return; - } - - const PointCloudXYZ cloud = generateBoxPointCloud(center, size, pointSpacing); - if (cloud.empty()) { - QMessageBox::warning(this, "提示", "生成点云失败,结果为空。"); - return; - } - - addGeneratedCloud(cloud, QString("GeneratedBox_%1x%2x%3") - .arg(size.x(), 0, 'f', 1) - .arg(size.y(), 0, 'f', 1) - .arg(size.z(), 0, 'f', 1)); -} - -void CloudViewMainWindow::onConvertEulerMatrix() -{ - QDialog* dialog = new QDialog(this); - dialog->setAttribute(Qt::WA_DeleteOnClose); - dialog->setWindowTitle("欧拉角与方向向量矩阵互转"); - dialog->setWindowFlags(dialog->windowFlags() | Qt::Tool); - - QVBoxLayout* layout = new QVBoxLayout(dialog); - - QLabel* tip = new QLabel( - "输入欧拉角(单位: 度)按所选旋转顺序得到 3x3 方向向量矩阵;也可以从矩阵反算欧拉角。\n" - "方向向量矩阵采用列向量约定: R = [ X轴 | Y轴 | Z轴 ],每列为该坐标轴在参考系中的单位方向向量。", - dialog); - tip->setWordWrap(true); - tip->setStyleSheet("color: gray;"); - layout->addWidget(tip); - - QGroupBox* eulerGroup = new QGroupBox("欧拉角 (deg)", dialog); - QGridLayout* eulerLayout = new QGridLayout(eulerGroup); - - auto makeAngleSpin = [](QWidget* parent) { - QDoubleSpinBox* spin = new QDoubleSpinBox(parent); - spin->setDecimals(6); - spin->setRange(-3600.0, 3600.0); - spin->setSingleStep(0.1); - spin->setValue(0.0); - spin->setAlignment(Qt::AlignRight); - spin->setButtonSymbols(QAbstractSpinBox::NoButtons); - return spin; - }; - - QDoubleSpinBox* eulerRx = makeAngleSpin(eulerGroup); - QDoubleSpinBox* eulerRy = makeAngleSpin(eulerGroup); - QDoubleSpinBox* eulerRz = makeAngleSpin(eulerGroup); - eulerLayout->addWidget(new QLabel("RX:", eulerGroup), 0, 0); - eulerLayout->addWidget(eulerRx, 0, 1); - eulerLayout->addWidget(new QLabel("RY:", eulerGroup), 1, 0); - eulerLayout->addWidget(eulerRy, 1, 1); - eulerLayout->addWidget(new QLabel("RZ:", eulerGroup), 2, 0); - eulerLayout->addWidget(eulerRz, 2, 1); - - QComboBox* comboOrder = new QComboBox(eulerGroup); - comboOrder->addItem("ZYX (Yaw-Pitch-Roll)", static_cast(EulerRotationOrder::ZYX)); - comboOrder->addItem("XYZ (Roll-Pitch-Yaw)", static_cast(EulerRotationOrder::XYZ)); - comboOrder->addItem("ZXY (Yaw-Roll-Pitch)", static_cast(EulerRotationOrder::ZXY)); - comboOrder->addItem("YXZ (Pitch-Roll-Yaw)", static_cast(EulerRotationOrder::YXZ)); - comboOrder->addItem("XZY (Roll-Yaw-Pitch)", static_cast(EulerRotationOrder::XZY)); - comboOrder->addItem("YZX (Pitch-Yaw-Roll)", static_cast(EulerRotationOrder::YZX)); - if (m_comboEulerOrder && m_comboEulerOrder->currentIndex() >= 0) { - comboOrder->setCurrentIndex(m_comboEulerOrder->currentIndex()); - } - eulerLayout->addWidget(new QLabel("旋转顺序:", eulerGroup), 3, 0); - eulerLayout->addWidget(comboOrder, 3, 1); - - QGroupBox* matrixGroup = new QGroupBox("方向向量矩阵 (3x3)", dialog); - QGridLayout* matLayout = new QGridLayout(matrixGroup); - - auto makeHeader = [matrixGroup](const QString& text) { - QLabel* label = new QLabel(text, matrixGroup); - label->setAlignment(Qt::AlignCenter); - label->setStyleSheet("font-weight: bold;"); - return label; - }; - - matLayout->addWidget(makeHeader("X轴"), 0, 1); - matLayout->addWidget(makeHeader("Y轴"), 0, 2); - matLayout->addWidget(makeHeader("Z轴"), 0, 3); - matLayout->addWidget(new QLabel("X分量:", matrixGroup), 1, 0); - matLayout->addWidget(new QLabel("Y分量:", matrixGroup), 2, 0); - matLayout->addWidget(new QLabel("Z分量:", matrixGroup), 3, 0); - - std::array, 3> matCells{}; - for (int c = 0; c < 3; ++c) { - for (int r = 0; r < 3; ++r) { - QDoubleSpinBox* spin = new QDoubleSpinBox(matrixGroup); - spin->setDecimals(6); - spin->setRange(-1.0, 1.0); - spin->setSingleStep(0.01); - spin->setAlignment(Qt::AlignRight); - spin->setButtonSymbols(QAbstractSpinBox::NoButtons); - spin->setValue(r == c ? 1.0 : 0.0); - matCells[r][c] = spin; - matLayout->addWidget(spin, r + 1, c + 1); - } - } - - layout->addWidget(eulerGroup); - layout->addWidget(matrixGroup); - - QHBoxLayout* actionLayout = new QHBoxLayout(); - QPushButton* eulerToMat = new QPushButton("欧拉角 -> 方向向量矩阵", dialog); - QPushButton* matToEuler = new QPushButton("方向向量矩阵 -> 欧拉角", dialog); - QPushButton* closeButton = new QPushButton("关闭", dialog); - actionLayout->addWidget(eulerToMat); - actionLayout->addWidget(matToEuler); - actionLayout->addWidget(closeButton); - layout->addLayout(actionLayout); - - connect(eulerToMat, &QPushButton::clicked, dialog, [=]() { - const EulerRotationOrder order = static_cast(comboOrder->currentData().toInt()); - const QMatrix4x4 rotation = buildEulerRotationMatrix(order, - eulerRx->value(), - eulerRy->value(), - eulerRz->value()); - for (int r = 0; r < 3; ++r) { - for (int c = 0; c < 3; ++c) { - matCells[r][c]->setValue(rotation(r, c)); - } - } - }); - - connect(matToEuler, &QPushButton::clicked, dialog, [=]() { - QMatrix4x4 rotation; - rotation.setToIdentity(); - for (int r = 0; r < 3; ++r) { - for (int c = 0; c < 3; ++c) { - rotation(r, c) = static_cast(matCells[r][c]->value()); - } - } - const EulerRotationOrder order = static_cast(comboOrder->currentData().toInt()); - const QVector3D euler = rotationMatrixToEuler(order, rotation); - eulerRx->setValue(euler.x()); - eulerRy->setValue(euler.y()); - eulerRz->setValue(euler.z()); - }); - - connect(closeButton, &QPushButton::clicked, dialog, &QDialog::close); - dialog->show(); -} - -void CloudViewMainWindow::onPoseCompensation() -{ - QDialog* dialog = new QDialog(this); - dialog->setAttribute(Qt::WA_DeleteOnClose); - dialog->setWindowTitle("姿态补偿计算"); - dialog->setWindowFlags(dialog->windowFlags() | Qt::Tool); - - QVBoxLayout* layout = new QVBoxLayout(dialog); - - QLabel* tip = new QLabel( - "输入当前欧拉角和期望欧拉角(单位: 度),计算出补偿角。\n" - "分量相减:补偿 = 期望 − 当前(仅小角度差/单轴旋转时近似有效,结果与顺序无关)\n" - "矩阵补偿:补偿 = 提取(R_cur^T × R_des)\n" - " · 输入顺序:当前/期望欧拉角的旋转顺序(取决于机器人/算法侧约定)\n" - " · 补偿输出顺序:反解 R_comp 用的旋转顺序,要与下游施加补偿的约定一致\n" - " (DetectPresenter::ApplyAxesRotation 硬编码 R = Rx·Ry·Rz,即 XYZ 内旋)", - dialog); - tip->setWordWrap(true); - tip->setStyleSheet("color: gray;"); - layout->addWidget(tip); - - auto fillEulerOrderItems = [](QComboBox* combo) { - combo->addItem("ZYX (Yaw-Pitch-Roll)", static_cast(EulerRotationOrder::ZYX)); - combo->addItem("XYZ (Roll-Pitch-Yaw)", static_cast(EulerRotationOrder::XYZ)); - combo->addItem("ZXY (Yaw-Roll-Pitch)", static_cast(EulerRotationOrder::ZXY)); - combo->addItem("YXZ (Pitch-Roll-Yaw)", static_cast(EulerRotationOrder::YXZ)); - combo->addItem("XZY (Roll-Yaw-Pitch)", static_cast(EulerRotationOrder::XZY)); - combo->addItem("YZX (Pitch-Yaw-Roll)", static_cast(EulerRotationOrder::YZX)); - }; - - QComboBox* comboInputOrder = new QComboBox(dialog); - fillEulerOrderItems(comboInputOrder); - if (m_comboEulerOrder && m_comboEulerOrder->currentIndex() >= 0) { - comboInputOrder->setCurrentIndex(m_comboEulerOrder->currentIndex()); - } - - QComboBox* comboOutputOrder = new QComboBox(dialog); - fillEulerOrderItems(comboOutputOrder); - // 默认 XYZ,匹配 DetectPresenter::ApplyAxesRotation 中 R = Rx·Ry·Rz 的硬编码 - const int outputXyzIndex = comboOutputOrder->findData(static_cast(EulerRotationOrder::XYZ)); - if (outputXyzIndex >= 0) { - comboOutputOrder->setCurrentIndex(outputXyzIndex); - } - - QHBoxLayout* orderLayout = new QHBoxLayout(); - orderLayout->addWidget(new QLabel("输入顺序:", dialog)); - orderLayout->addWidget(comboInputOrder); - orderLayout->addSpacing(20); - orderLayout->addWidget(new QLabel("补偿输出顺序:", dialog)); - orderLayout->addWidget(comboOutputOrder); - orderLayout->addStretch(); - layout->addLayout(orderLayout); - - QComboBox* comboMethod = new QComboBox(dialog); - comboMethod->addItem("分量相减(期望 − 当前)"); - comboMethod->addItem("矩阵补偿(R_cur^T × R_des)"); - - QHBoxLayout* methodLayout = new QHBoxLayout(); - methodLayout->addWidget(new QLabel("计算方法:", dialog)); - methodLayout->addWidget(comboMethod); - methodLayout->addStretch(); - layout->addLayout(methodLayout); - - auto makeAngleSpin = [](QWidget* parent) { - QDoubleSpinBox* spin = new QDoubleSpinBox(parent); - spin->setDecimals(6); - spin->setRange(-3600.0, 3600.0); - spin->setSingleStep(0.1); - spin->setValue(0.0); - spin->setAlignment(Qt::AlignRight); - spin->setButtonSymbols(QAbstractSpinBox::NoButtons); - return spin; - }; - - QGroupBox* currentGroup = new QGroupBox("当前欧拉角 (deg)", dialog); - QGridLayout* currentLayout = new QGridLayout(currentGroup); - QDoubleSpinBox* curRx = makeAngleSpin(currentGroup); - QDoubleSpinBox* curRy = makeAngleSpin(currentGroup); - QDoubleSpinBox* curRz = makeAngleSpin(currentGroup); - currentLayout->addWidget(new QLabel("RX:", currentGroup), 0, 0); - currentLayout->addWidget(curRx, 0, 1); - currentLayout->addWidget(new QLabel("RY:", currentGroup), 1, 0); - currentLayout->addWidget(curRy, 1, 1); - currentLayout->addWidget(new QLabel("RZ:", currentGroup), 2, 0); - currentLayout->addWidget(curRz, 2, 1); - - QGroupBox* desiredGroup = new QGroupBox("期望欧拉角 (deg)", dialog); - QGridLayout* desiredLayout = new QGridLayout(desiredGroup); - QDoubleSpinBox* desRx = makeAngleSpin(desiredGroup); - QDoubleSpinBox* desRy = makeAngleSpin(desiredGroup); - QDoubleSpinBox* desRz = makeAngleSpin(desiredGroup); - desiredLayout->addWidget(new QLabel("RX:", desiredGroup), 0, 0); - desiredLayout->addWidget(desRx, 0, 1); - desiredLayout->addWidget(new QLabel("RY:", desiredGroup), 1, 0); - desiredLayout->addWidget(desRy, 1, 1); - desiredLayout->addWidget(new QLabel("RZ:", desiredGroup), 2, 0); - desiredLayout->addWidget(desRz, 2, 1); - - QGroupBox* resultGroup = new QGroupBox("补偿欧拉角 (deg)", dialog); - QGridLayout* resultLayout = new QGridLayout(resultGroup); - QLineEdit* resultRx = new QLineEdit(resultGroup); - QLineEdit* resultRy = new QLineEdit(resultGroup); - QLineEdit* resultRz = new QLineEdit(resultGroup); - resultRx->setReadOnly(true); - resultRy->setReadOnly(true); - resultRz->setReadOnly(true); - resultRx->setAlignment(Qt::AlignRight); - resultRy->setAlignment(Qt::AlignRight); - resultRz->setAlignment(Qt::AlignRight); - resultRx->setStyleSheet("background-color: #f0f0f0; font-weight: bold;"); - resultRy->setStyleSheet("background-color: #f0f0f0; font-weight: bold;"); - resultRz->setStyleSheet("background-color: #f0f0f0; font-weight: bold;"); - resultLayout->addWidget(new QLabel("RX:", resultGroup), 0, 0); - resultLayout->addWidget(resultRx, 0, 1); - resultLayout->addWidget(new QLabel("RY:", resultGroup), 1, 0); - resultLayout->addWidget(resultRy, 1, 1); - resultLayout->addWidget(new QLabel("RZ:", resultGroup), 2, 0); - resultLayout->addWidget(resultRz, 2, 1); - - layout->addWidget(currentGroup); - layout->addWidget(desiredGroup); - layout->addWidget(resultGroup); - - QHBoxLayout* actionLayout = new QHBoxLayout(); - QPushButton* calcButton = new QPushButton("计算补偿", dialog); - QPushButton* closeButton = new QPushButton("关闭", dialog); - actionLayout->addStretch(); - actionLayout->addWidget(calcButton); - actionLayout->addWidget(closeButton); - layout->addLayout(actionLayout); - - connect(calcButton, &QPushButton::clicked, dialog, [=]() { - if (comboMethod->currentIndex() == 0) { - // 分量相减 - resultRx->setText(QString::number(desRx->value() - curRx->value(), 'f', 6)); - resultRy->setText(QString::number(desRy->value() - curRy->value(), 'f', 6)); - resultRz->setText(QString::number(desRz->value() - curRz->value(), 'f', 6)); - } else { - // 矩阵补偿 - const EulerRotationOrder inputOrder = static_cast(comboInputOrder->currentData().toInt()); - const EulerRotationOrder outputOrder = static_cast(comboOutputOrder->currentData().toInt()); - - // 输入欧拉角按 inputOrder 还原成旋转矩阵 - const QMatrix4x4 curMat = buildEulerRotationMatrix(inputOrder, curRx->value(), curRy->value(), curRz->value()); - const QMatrix4x4 desMat = buildEulerRotationMatrix(inputOrder, desRx->value(), desRy->value(), desRz->value()); - - // R_des = R_cur * R_comp(工具坐标系补偿,R_comp = R_cur^T * R_des) - const QMatrix4x4 compMat = curMat.transposed() * desMat; - - // 补偿矩阵按 outputOrder 反解(要与下游施加补偿的旋转顺序一致) - const QVector3D euler = rotationMatrixToEuler(outputOrder, compMat); - resultRx->setText(QString::number(euler.x(), 'f', 6)); - resultRy->setText(QString::number(euler.y(), 'f', 6)); - resultRz->setText(QString::number(euler.z(), 'f', 6)); - } - }); - - connect(closeButton, &QPushButton::clicked, dialog, &QDialog::close); - dialog->show(); -} - -void CloudViewMainWindow::onEulerOrderChanged(int index) -{ - if (!m_glWidget) { - return; - } - - EulerRotationOrder order = static_cast(m_comboEulerOrder->itemData(index).toInt()); - m_glWidget->setEulerRotationOrder(order); - - // 如果有姿态点,刷新显示 - m_glWidget->update(); - - QString orderName = m_comboEulerOrder->currentText(); - statusBar()->showMessage(QString("欧拉角旋转顺序已切换为: %1").arg(orderName)); - LOG_INFO("[CloudView] Euler rotation order changed to: %s\n", orderName.toStdString().c_str()); -} - -void CloudViewMainWindow::onViewAnglesChanged(float rotX, float rotY, float rotZ) -{ - // 更新显示的角度值 - m_editRotX->setText(QString::number(rotX, 'f', 1)); - m_editRotY->setText(QString::number(rotY, 'f', 1)); - m_editRotZ->setText(QString::number(rotZ, 'f', 1)); -} - -void CloudViewMainWindow::onPoint1CoordChanged() -{ - // 读取编辑框中的坐标 - bool ok = true; - float x = m_editPoint1X->text().toFloat(&ok); - if (!ok) { - QMessageBox::warning(this, "错误", "点1 X 值无效"); - return; - } - - float y = m_editPoint1Y->text().toFloat(&ok); - if (!ok) { - QMessageBox::warning(this, "错误", "点1 Y 值无效"); - return; - } - - float z = m_editPoint1Z->text().toFloat(&ok); - if (!ok) { - QMessageBox::warning(this, "错误", "点1 Z 值无效"); - return; - } - - // 直接设置选中点坐标(无论是否已有鼠标选点) - m_glWidget->setSelectedPointCoord(0, x, y, z); - updateSelectedPointsDisplay(); - - // 如果启用了测距且有两个点,重新计算距离 - auto updatedPoints = m_glWidget->getSelectedPoints(); - if (m_glWidget->isMeasureDistanceEnabled() && updatedPoints.size() >= 2 && updatedPoints[1].valid) { - float distance = m_glWidget->calculateDistance(updatedPoints[0], updatedPoints[1]) * 2.0f; - m_lblDistance->setText(QString("%1 mm").arg(distance, 0, 'f', 3)); - statusBar()->showMessage(QString("点1坐标已更新,距离: %1 mm").arg(distance, 0, 'f', 3)); - } else { - statusBar()->showMessage(QString("点1坐标已更新为 (%1, %2, %3)").arg(x, 0, 'f', 3).arg(y, 0, 'f', 3).arg(z, 0, 'f', 3)); - } - - LOG_INFO("[CloudView] Point1 coord updated to (%.3f, %.3f, %.3f)\n", x, y, z); -} - -void CloudViewMainWindow::onPoint2CoordChanged() -{ - // 读取编辑框中的坐标 - bool ok = true; - float x = m_editPoint2X->text().toFloat(&ok); - if (!ok) { - QMessageBox::warning(this, "错误", "点2 X 值无效"); - return; - } - - float y = m_editPoint2Y->text().toFloat(&ok); - if (!ok) { - QMessageBox::warning(this, "错误", "点2 Y 值无效"); - return; - } - - float z = m_editPoint2Z->text().toFloat(&ok); - if (!ok) { - QMessageBox::warning(this, "错误", "点2 Z 值无效"); - return; - } - - // 直接设置选中点坐标(无论是否已有鼠标选点) - m_glWidget->setSelectedPointCoord(1, x, y, z); - updateSelectedPointsDisplay(); - - // 如果启用了测距,重新计算距离 - auto updatedPoints = m_glWidget->getSelectedPoints(); - if (m_glWidget->isMeasureDistanceEnabled() && updatedPoints.size() >= 2 && updatedPoints[0].valid) { - float distance = m_glWidget->calculateDistance(updatedPoints[0], updatedPoints[1]); - m_lblDistance->setText(QString("%1 mm").arg(distance, 0, 'f', 3)); - statusBar()->showMessage(QString("点2坐标已更新,距离: %1 mm").arg(distance, 0, 'f', 3)); - } else { - statusBar()->showMessage(QString("点2坐标已更新为 (%1, %2, %3)").arg(x, 0, 'f', 3).arg(y, 0, 'f', 3).arg(z, 0, 'f', 3)); - } - - LOG_INFO("[CloudView] Point2 coord updated to (%.3f, %.3f, %.3f)\n", x, y, z); -} - + +void CloudViewMainWindow::onLineSelectModeChanged(bool checked) +{ + if (checked) { + // 纵向模式 + m_glWidget->setLineSelectMode(LineSelectMode::Vertical); + } else { + // 横向模式 + m_glWidget->setLineSelectMode(LineSelectMode::Horizontal); + } + m_lineNumberInput->setPlaceholderText("输入索引"); +} + +void CloudViewMainWindow::onClearLinePoints() +{ + m_glWidget->clearSelectedLine(); + m_glWidget->clearListHighlightPoint(); // 清除列表选中的高亮点 + m_lblLineIndex->setText("--"); + m_lblLinePointCount->setText("--"); + statusBar()->showMessage("已清除选线"); +} + +void CloudViewMainWindow::onLineSelected(const SelectedLineInfo& line) +{ + // 重新选线时清除列表高亮点 + m_glWidget->clearListHighlightPoint(); + + if (!line.valid) { + m_lblLineIndex->setText("--"); + m_lblLinePointCount->setText("--"); + return; + } + + // 状态栏显示:线号/索引号、线点数 + if (line.mode == LineSelectMode::Vertical) { + m_lblLineIndex->setText(QString::number(line.lineIndex)); + statusBar()->showMessage(QString("选中线 | 线号: %1 | 线点数: %2") + .arg(line.lineIndex) + .arg(line.pointCount)); + } else { + // 横向选线:显示索引号 + m_lblLineIndex->setText(QString::number(line.pointIndex)); + statusBar()->showMessage(QString("选中横向线 | 索引号: %1 | 线点数: %2") + .arg(line.pointIndex) + .arg(line.pointCount)); + } + m_lblLinePointCount->setText(QString::number(line.pointCount)); + + // 如果线上点对话框已打开,刷新内容 + updateLinePointsDialog(); +} + +void CloudViewMainWindow::onSelectLineByNumber() +{ + if (m_glWidget->getCloudCount() == 0) { + QMessageBox::warning(this, "提示", "请先加载点云"); + return; + } + + QString text = m_lineNumberInput->text().trimmed(); + if (text.isEmpty()) { + QMessageBox::warning(this, "提示", "请输入索引"); + return; + } + + bool ok; + int index = text.toInt(&ok); + if (!ok || index < 0) { + QMessageBox::warning(this, "提示", "请输入有效的索引(非负整数)"); + return; + } + + bool success = false; + if (m_rbVertical->isChecked()) { + // 纵向选线:直接使用索引 + success = m_glWidget->selectLineByIndex(index); + if (!success) { + QMessageBox::warning(this, "提示", QString("索引 %1 不存在").arg(index)); + } + } else { + // 横向选线:直接使用索引 + success = m_glWidget->selectHorizontalLineByIndex(index); + if (!success) { + QMessageBox::warning(this, "提示", QString("索引 %1 不存在").arg(index)); + } + } +} + +QVector CloudViewMainWindow::getOriginalLinePoints(const SelectedLineInfo& lineInfo) +{ + QVector points; + + if (!lineInfo.valid || m_originalCloud.empty()) { + return points; + } + + if (lineInfo.mode == LineSelectMode::Vertical) { + // 纵向选线:获取同一条扫描线上的所有点 + for (size_t i = 0; i < m_originalCloud.points.size(); ++i) { + if (i < m_originalCloud.lineIndices.size() && + m_originalCloud.lineIndices[i] == lineInfo.lineIndex) { + const auto& pt = m_originalCloud.points[i]; + points.append(QVector3D(pt.x, pt.y, pt.z)); + } + } + } else { + // 横向选线:获取所有线的相同索引点 + if (m_currentLinePtNum > 0 && lineInfo.pointIndex >= 0) { + for (size_t i = 0; i < m_originalCloud.points.size(); ++i) { + int originalIdx = static_cast(i); + if (originalIdx % m_currentLinePtNum == lineInfo.pointIndex) { + const auto& pt = m_originalCloud.points[i]; + points.append(QVector3D(pt.x, pt.y, pt.z)); + } + } + } + } + + return points; +} + +void CloudViewMainWindow::updateLinePointsDialog() +{ + if (!m_linePointsDialog || !m_linePointsTable) { + return; + } + + SelectedLineInfo lineInfo = m_glWidget->getSelectedLine(); + if (!lineInfo.valid) { + m_linePointsTable->setRowCount(0); + m_linePointsDialog->setWindowTitle("线上点坐标"); + m_currentLinePoints.clear(); + return; + } + + // 从原始数据获取线上点(包含0,0,0) + m_currentLinePoints = getOriginalLinePoints(lineInfo); + + // 更新标题 + QString title; + if (lineInfo.mode == LineSelectMode::Vertical) { + title = QString("线上点坐标 - 线号: %1 (共 %2 个点)") + .arg(lineInfo.lineIndex) + .arg(m_currentLinePoints.size()); + } else { + title = QString("线上点坐标 - 索引号: %1 (共 %2 个点)") + .arg(lineInfo.pointIndex) + .arg(m_currentLinePoints.size()); + } + m_linePointsDialog->setWindowTitle(title); + + // 更新表格 + m_linePointsTable->setRowCount(m_currentLinePoints.size()); + + // 斑马线颜色 + QColor evenColor(245, 245, 245); // 浅灰色 + QColor oddColor(255, 255, 255); // 白色 + + for (int i = 0; i < m_currentLinePoints.size(); ++i) { + const QVector3D& pt = m_currentLinePoints[i]; + QColor rowColor = (i % 2 == 0) ? evenColor : oddColor; + + // 序号 + QTableWidgetItem* indexItem = new QTableWidgetItem(QString::number(i)); + indexItem->setTextAlignment(Qt::AlignCenter); + indexItem->setBackground(rowColor); + indexItem->setFlags(indexItem->flags() & ~Qt::ItemIsEditable); + m_linePointsTable->setItem(i, 0, indexItem); + + // X + QTableWidgetItem* xItem = new QTableWidgetItem(QString::number(pt.x(), 'f', 3)); + xItem->setTextAlignment(Qt::AlignCenter); + xItem->setBackground(rowColor); + xItem->setFlags(xItem->flags() & ~Qt::ItemIsEditable); + m_linePointsTable->setItem(i, 1, xItem); + + // Y + QTableWidgetItem* yItem = new QTableWidgetItem(QString::number(pt.y(), 'f', 3)); + yItem->setTextAlignment(Qt::AlignCenter); + yItem->setBackground(rowColor); + yItem->setFlags(yItem->flags() & ~Qt::ItemIsEditable); + m_linePointsTable->setItem(i, 2, yItem); + + // Z + QTableWidgetItem* zItem = new QTableWidgetItem(QString::number(pt.z(), 'f', 3)); + zItem->setTextAlignment(Qt::AlignCenter); + zItem->setBackground(rowColor); + zItem->setFlags(zItem->flags() & ~Qt::ItemIsEditable); + m_linePointsTable->setItem(i, 3, zItem); + } +} + +void CloudViewMainWindow::onShowLinePoints() +{ + SelectedLineInfo lineInfo = m_glWidget->getSelectedLine(); + if (!lineInfo.valid) { + QMessageBox::warning(this, "提示", "请先选择一条线"); + return; + } + + // 如果对话框已存在,刷新内容并显示 + if (m_linePointsDialog) { + updateLinePointsDialog(); + m_linePointsDialog->raise(); + m_linePointsDialog->activateWindow(); + return; + } + + // 创建对话框 + m_linePointsDialog = new QDialog(this); + m_linePointsDialog->resize(450, 500); + m_linePointsDialog->setAttribute(Qt::WA_DeleteOnClose); + + // 对话框关闭时清理指针 + connect(m_linePointsDialog, &QDialog::destroyed, this, [this]() { + m_linePointsDialog = nullptr; + m_linePointsTable = nullptr; + m_currentLinePoints.clear(); + m_glWidget->clearListHighlightPoint(); + }); + + QVBoxLayout* layout = new QVBoxLayout(m_linePointsDialog); + + // 提示标签 + QLabel* lblTip = new QLabel("点击行在3D视图中高亮显示", m_linePointsDialog); + lblTip->setStyleSheet("color: gray; font-size: 10px;"); + layout->addWidget(lblTip); + + // 创建表格控件 + m_linePointsTable = new QTableWidget(m_linePointsDialog); + m_linePointsTable->setColumnCount(4); + m_linePointsTable->setHorizontalHeaderLabels({"序号", "X", "Y", "Z"}); + m_linePointsTable->setFont(QFont("Consolas", 9)); + m_linePointsTable->setSelectionBehavior(QAbstractItemView::SelectRows); + m_linePointsTable->setSelectionMode(QAbstractItemView::SingleSelection); + m_linePointsTable->verticalHeader()->setVisible(false); + + // 设置列宽 + m_linePointsTable->setColumnWidth(0, 60); // 序号 + m_linePointsTable->setColumnWidth(1, 110); // X + m_linePointsTable->setColumnWidth(2, 110); // Y + m_linePointsTable->setColumnWidth(3, 110); // Z + + // 表头样式 + m_linePointsTable->horizontalHeader()->setStretchLastSection(true); + m_linePointsTable->horizontalHeader()->setDefaultAlignment(Qt::AlignCenter); + + connect(m_linePointsTable, &QTableWidget::cellClicked, + this, &CloudViewMainWindow::onLinePointTableClicked); + layout->addWidget(m_linePointsTable); + + // 关闭按钮 + QPushButton* btnClose = new QPushButton("关闭", m_linePointsDialog); + connect(btnClose, &QPushButton::clicked, m_linePointsDialog, &QDialog::close); + layout->addWidget(btnClose); + + // 填充数据 + updateLinePointsDialog(); + + m_linePointsDialog->show(); +} + +void CloudViewMainWindow::onLinePointTableClicked(int row, int column) +{ + Q_UNUSED(column); + + if (row >= 0 && row < m_currentLinePoints.size()) { + const QVector3D& pt = m_currentLinePoints[row]; + m_glWidget->setListHighlightPoint(pt); + + // 在状态栏显示选中点信息 + statusBar()->showMessage(QString("列表选中点 %1: (%2, %3, %4)") + .arg(row) + .arg(pt.x(), 0, 'f', 3) + .arg(pt.y(), 0, 'f', 3) + .arg(pt.z(), 0, 'f', 3)); + } +} + +void CloudViewMainWindow::onShowPose1() +{ + // 从输入框读取点1坐标 + bool ok = true; + float x1 = m_editPoint1X->text().toFloat(&ok); + if (!ok) { QMessageBox::warning(this, "错误", "点1 X 值无效"); return; } + float y1 = m_editPoint1Y->text().toFloat(&ok); + if (!ok) { QMessageBox::warning(this, "错误", "点1 Y 值无效"); return; } + float z1 = m_editPoint1Z->text().toFloat(&ok); + if (!ok) { QMessageBox::warning(this, "错误", "点1 Z 值无效"); return; } + + // 设置选中点(不存在则创建) + m_glWidget->setSelectedPointCoord(0, x1, y1, z1); + + // 读取姿态参数 + float rx = m_editRx1->text().toFloat(&ok); + if (!ok) { + QMessageBox::warning(this, "错误", "点1 RX 值无效"); + return; + } + + float ry = m_editRy1->text().toFloat(&ok); + if (!ok) { + QMessageBox::warning(this, "错误", "点1 RY 值无效"); + return; + } + + float rz = m_editRz1->text().toFloat(&ok); + if (!ok) { + QMessageBox::warning(this, "错误", "点1 RZ 值无效"); + return; + } + + // 读取坐标轴缩放 + float scale = m_editPoseScale->text().toFloat(&ok); + if (!ok || scale <= 0) scale = 25.0f; + + // 清除之前的姿态点 + m_glWidget->clearPosePoints(); + + // 创建点1的姿态点 + PosePoint pose1(x1, y1, z1, rx, ry, rz, scale); + QVector poses; + poses.append(pose1); + + // 如果点2输入框有值,也一起显示 + bool ok2 = true; + float x2 = m_editPoint2X->text().toFloat(&ok2); + float y2 = ok2 ? m_editPoint2Y->text().toFloat(&ok2) : 0; + float z2 = ok2 ? m_editPoint2Z->text().toFloat(&ok2) : 0; + if (ok2 && !(x2 == 0 && y2 == 0 && z2 == 0 && m_editPoint2X->text().isEmpty())) { + m_glWidget->setSelectedPointCoord(1, x2, y2, z2); + + bool okR = true; + float rx2 = m_editRx2->text().toFloat(&okR); + float ry2 = okR ? m_editRy2->text().toFloat(&okR) : 0; + float rz2 = okR ? m_editRz2->text().toFloat(&okR) : 0; + if (okR) { + PosePoint pose2(x2, y2, z2, rx2, ry2, rz2, scale); + poses.append(pose2); + } + } + + // 添加到显示 + m_glWidget->addPosePoints(poses); + + statusBar()->showMessage(QString("已显示点1姿态 (%1, %2, %3) 旋转(%4°, %5°, %6°)") + .arg(x1, 0, 'f', 3).arg(y1, 0, 'f', 3).arg(z1, 0, 'f', 3) + .arg(rx, 0, 'f', 1).arg(ry, 0, 'f', 1).arg(rz, 0, 'f', 1)); + + LOG_INFO("[CloudView] Show pose1 at (%.3f, %.3f, %.3f) with rotation (%.1f, %.1f, %.1f)\n", + x1, y1, z1, rx, ry, rz); +} + +void CloudViewMainWindow::onShowPose2() +{ + // 从输入框读取点2坐标 + bool ok = true; + float x2 = m_editPoint2X->text().toFloat(&ok); + if (!ok) { QMessageBox::warning(this, "错误", "点2 X 值无效"); return; } + float y2 = m_editPoint2Y->text().toFloat(&ok); + if (!ok) { QMessageBox::warning(this, "错误", "点2 Y 值无效"); return; } + float z2 = m_editPoint2Z->text().toFloat(&ok); + if (!ok) { QMessageBox::warning(this, "错误", "点2 Z 值无效"); return; } + + // 设置选中点(不存在则创建) + m_glWidget->setSelectedPointCoord(1, x2, y2, z2); + + // 读取姿态参数 + float rx = m_editRx2->text().toFloat(&ok); + if (!ok) { + QMessageBox::warning(this, "错误", "点2 RX 值无效"); + return; + } + + float ry = m_editRy2->text().toFloat(&ok); + if (!ok) { + QMessageBox::warning(this, "错误", "点2 RY 值无效"); + return; + } + + float rz = m_editRz2->text().toFloat(&ok); + if (!ok) { + QMessageBox::warning(this, "错误", "点2 RZ 值无效"); + return; + } + + // 读取坐标轴缩放 + float scale = m_editPoseScale->text().toFloat(&ok); + if (!ok || scale <= 0) scale = 25.0f; + + // 清除之前的姿态点 + m_glWidget->clearPosePoints(); + + QVector poses; + + // 如果点1输入框有值,也一起显示 + bool ok1 = true; + float x1 = m_editPoint1X->text().toFloat(&ok1); + float y1 = ok1 ? m_editPoint1Y->text().toFloat(&ok1) : 0; + float z1 = ok1 ? m_editPoint1Z->text().toFloat(&ok1) : 0; + if (ok1 && !(x1 == 0 && y1 == 0 && z1 == 0 && m_editPoint1X->text().isEmpty())) { + m_glWidget->setSelectedPointCoord(0, x1, y1, z1); + + bool okR = true; + float rx1 = m_editRx1->text().toFloat(&okR); + float ry1 = okR ? m_editRy1->text().toFloat(&okR) : 0; + float rz1 = okR ? m_editRz1->text().toFloat(&okR) : 0; + if (okR) { + PosePoint pose1(x1, y1, z1, rx1, ry1, rz1, scale); + poses.append(pose1); + } + } + + // 创建点2的姿态点 + PosePoint pose2(x2, y2, z2, rx, ry, rz, scale); + poses.append(pose2); + + // 添加到显示 + m_glWidget->addPosePoints(poses); + + statusBar()->showMessage(QString("已显示点2姿态 (%1, %2, %3) 旋转(%4°, %5°, %6°)") + .arg(x2, 0, 'f', 3).arg(y2, 0, 'f', 3).arg(z2, 0, 'f', 3) + .arg(rx, 0, 'f', 1).arg(ry, 0, 'f', 1).arg(rz, 0, 'f', 1)); + + LOG_INFO("[CloudView] Show pose2 at (%.3f, %.3f, %.3f) with rotation (%.1f, %.1f, %.1f)\n", + x2, y2, z2, rx, ry, rz); +} + +void CloudViewMainWindow::onShowInputLine() +{ + // 读取点1坐标 + bool ok = true; + float x1 = m_editLineX1->text().toFloat(&ok); + if (!ok) { + QMessageBox::warning(this, "错误", "点1 X 值无效"); + return; + } + + float y1 = m_editLineY1->text().toFloat(&ok); + if (!ok) { + QMessageBox::warning(this, "错误", "点1 Y 值无效"); + return; + } + + float z1 = m_editLineZ1->text().toFloat(&ok); + if (!ok) { + QMessageBox::warning(this, "错误", "点1 Z 值无效"); + return; + } + + // 读取点2坐标 + float x2 = m_editLineX2->text().toFloat(&ok); + if (!ok) { + QMessageBox::warning(this, "错误", "点2 X 值无效"); + return; + } + + float y2 = m_editLineY2->text().toFloat(&ok); + if (!ok) { + QMessageBox::warning(this, "错误", "点2 Y 值无效"); + return; + } + + float z2 = m_editLineZ2->text().toFloat(&ok); + if (!ok) { + QMessageBox::warning(this, "错误", "点2 Z 值无效"); + return; + } + + // 清除之前的线段 + m_glWidget->clearLineSegments(); + + // 创建线段(红色) + LineSegment segment(x1, y1, z1, x2, y2, z2, 1.0f, 0.0f, 0.0f); + QVector segments; + segments.append(segment); + + // 添加到显示 + m_glWidget->addLineSegments(segments); + + // 计算距离 + float dx = x2 - x1; + float dy = y2 - y1; + float dz = z2 - z1; + float distance = std::sqrt(dx * dx + dy * dy + dz * dz); + + statusBar()->showMessage(QString("已显示线段 (%1,%2,%3) → (%4,%5,%6) 长度: %7") + .arg(x1, 0, 'f', 1).arg(y1, 0, 'f', 1).arg(z1, 0, 'f', 1) + .arg(x2, 0, 'f', 1).arg(y2, 0, 'f', 1).arg(z2, 0, 'f', 1) + .arg(distance, 0, 'f', 3)); + + LOG_INFO("[CloudView] Show input line from (%.3f, %.3f, %.3f) to (%.3f, %.3f, %.3f) length=%.3f\n", + x1, y1, z1, x2, y2, z2, distance); +} + +void CloudViewMainWindow::onClearInputLine() +{ + m_glWidget->clearLineSegments(); + statusBar()->showMessage(QString("已清除输入的线段")); +} + +void CloudViewMainWindow::onEyeToHandTransform() +{ + if (m_glWidget->getCloudCount() == 0) { + QMessageBox::warning(this, "提示", "请先加载点云"); + return; + } + + QDialog dialog(this); + dialog.setWindowTitle("眼在手外 - 矩阵变换"); + dialog.setModal(true); + + QVBoxLayout* layout = new QVBoxLayout(&dialog); + + QLabel* tip = new QLabel("输入或从文件加载 4x4 变换矩阵(相机坐标系 -> 基坐标系),直接应用到所有点云。", &dialog); + tip->setWordWrap(true); + tip->setStyleSheet("color: gray; font-size: 10px;"); + layout->addWidget(tip); + + QLabel* lblMatrix = new QLabel("变换矩阵 (4x4):", &dialog); + lblMatrix->setStyleSheet("font-weight: bold;"); + layout->addWidget(lblMatrix); + + QTextEdit* matrixEdit = new QTextEdit(&dialog); + matrixEdit->setFont(QFont("Consolas", 10)); + matrixEdit->setMinimumHeight(100); + matrixEdit->setMaximumHeight(120); + matrixEdit->setPlainText( + "1.0 0.0 0.0 0.0\n" + "0.0 1.0 0.0 0.0\n" + "0.0 0.0 1.0 0.0\n" + "0.0 0.0 0.0 1.0"); + layout->addWidget(matrixEdit); + + QPushButton* btnLoad = new QPushButton("从文件加载矩阵", &dialog); + connect(btnLoad, &QPushButton::clicked, [this, matrixEdit]() { + const QString fileName = QFileDialog::getOpenFileName( + this, "打开矩阵文件", QString(), + "标定矩阵文件 (*.ini *.txt);;INI文件 (*.ini);;文本文件 (*.txt);;所有文件 (*.*)"); + if (fileName.isEmpty()) return; + + if (fileName.endsWith(".ini", Qt::CaseInsensitive)) { + QVector> matrixList; + QString errorMessage; + if (!parseCalibIniFile(fileName, matrixList, &errorMessage)) { + QMessageBox::warning(this, "格式错误", errorMessage); + return; + } + if (matrixList.size() == 1) { + matrixEdit->setPlainText(formatMatrixText(matrixList[0].second)); + return; + } + QDialog dlg(this); + dlg.setWindowTitle("选择标定矩阵"); + QVBoxLayout* l = new QVBoxLayout(&dlg); + l->addWidget(new QLabel(QString("文件中包含 %1 个标定矩阵,请选择:").arg(matrixList.size()), &dlg)); + QListWidget* lw = new QListWidget(&dlg); + for (auto& m : matrixList) lw->addItem(m.first); + lw->setCurrentRow(0); + l->addWidget(lw); + QDialogButtonBox* bb = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dlg); + l->addWidget(bb); + connect(bb, &QDialogButtonBox::accepted, &dlg, &QDialog::accept); + connect(bb, &QDialogButtonBox::rejected, &dlg, &QDialog::reject); + if (dlg.exec() == QDialog::Accepted && lw->currentRow() >= 0 && lw->currentRow() < matrixList.size()) + matrixEdit->setPlainText(formatMatrixText(matrixList[lw->currentRow()].second)); + return; + } + + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + QMessageBox::critical(this, "错误", QString("无法打开文件: %1").arg(fileName)); + return; + } + QTextStream in(&file); + QString text = in.readAll(); + file.close(); + QMatrix4x4 matrix; + QString errorMessage; + if (!parseMatrixText(text, matrix, &errorMessage)) { + QMessageBox::warning(this, "格式错误", errorMessage); + return; + } + matrixEdit->setPlainText(formatMatrixText(matrix)); + }); + layout->addWidget(btnLoad); + + QLabel* lblFormat = new QLabel("矩阵文件格式:4 行,每行 4 个数值,分隔符:空格 / Tab / 逗号", &dialog); + lblFormat->setWordWrap(true); + lblFormat->setStyleSheet("color: gray; font-size: 9px;"); + layout->addWidget(lblFormat); + + QDialogButtonBox* btnBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); + btnBox->button(QDialogButtonBox::Ok)->setText("应用变换"); + btnBox->button(QDialogButtonBox::Ok)->setStyleSheet("QPushButton { background-color: #4CAF50; color: white; font-weight: bold; }" + "QPushButton:hover { background-color: #45a049; }"); + layout->addWidget(btnBox); + connect(btnBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + connect(btnBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + + if (dialog.exec() != QDialog::Accepted) return; + + QMatrix4x4 matrix; + QString errorMessage; + if (!parseMatrixText(matrixEdit->toPlainText().trimmed(), matrix, &errorMessage)) { + QMessageBox::warning(this, "格式错误", errorMessage); + return; + } + if (matrix.isIdentity()) { + QMessageBox::information(this, "提示", "当前矩阵为单位矩阵,无需变换"); + return; + } + + applyTransformToAllClouds(matrix); + statusBar()->showMessage("已将眼在手外矩阵变换应用到所有点云"); + LOG_INFO("[CloudView] Applied eye-to-hand matrix transform to all point clouds\n"); +} + +void CloudViewMainWindow::onEyeInHandTransform() +{ + if (m_glWidget->getCloudCount() == 0) { + QMessageBox::warning(this, "提示", "请先加载点云"); + return; + } + + QDialog dialog(this); + dialog.setWindowTitle("眼在手上 - 矩阵变换"); + dialog.setModal(true); + + QVBoxLayout* layout = new QVBoxLayout(&dialog); + + QLabel* tip = new QLabel( + "使用 4x4 矩阵作为“相机坐标系 -> 末端坐标系”的手眼矩阵。
" + "输入当前机械臂位姿(单位:mm / deg)后,将所有点云转换到机械臂基坐标系。
" + "角度组合方式:ZYX(Rz-Ry-Rx)。", &dialog); + tip->setWordWrap(true); + tip->setTextFormat(Qt::RichText); + tip->setStyleSheet("color: gray; font-size: 10px;"); + layout->addWidget(tip); + + QLabel* lblMatrix = new QLabel("手眼矩阵 (4x4):", &dialog); + lblMatrix->setStyleSheet("font-weight: bold;"); + layout->addWidget(lblMatrix); + + QTextEdit* matrixEdit = new QTextEdit(&dialog); + matrixEdit->setFont(QFont("Consolas", 10)); + matrixEdit->setMinimumHeight(100); + matrixEdit->setMaximumHeight(120); + matrixEdit->setPlainText( + "1.0 0.0 0.0 0.0\n" + "0.0 1.0 0.0 0.0\n" + "0.0 0.0 1.0 0.0\n" + "0.0 0.0 0.0 1.0"); + layout->addWidget(matrixEdit); + + QPushButton* btnLoad = new QPushButton("从文件加载矩阵", &dialog); + connect(btnLoad, &QPushButton::clicked, [this, matrixEdit]() { + const QString fileName = QFileDialog::getOpenFileName( + this, "打开矩阵文件", QString(), + "标定矩阵文件 (*.ini *.txt);;INI文件 (*.ini);;文本文件 (*.txt);;所有文件 (*.*)"); + if (fileName.isEmpty()) return; + + if (fileName.endsWith(".ini", Qt::CaseInsensitive)) { + QVector> matrixList; + QString errorMessage; + if (!parseCalibIniFile(fileName, matrixList, &errorMessage)) { + QMessageBox::warning(this, "格式错误", errorMessage); + return; + } + if (matrixList.size() == 1) { + matrixEdit->setPlainText(formatMatrixText(matrixList[0].second)); + return; + } + QDialog dlg(this); + dlg.setWindowTitle("选择标定矩阵"); + QVBoxLayout* l = new QVBoxLayout(&dlg); + l->addWidget(new QLabel(QString("文件中包含 %1 个标定矩阵,请选择:").arg(matrixList.size()), &dlg)); + QListWidget* lw = new QListWidget(&dlg); + for (auto& m : matrixList) lw->addItem(m.first); + lw->setCurrentRow(0); + l->addWidget(lw); + QDialogButtonBox* bb = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dlg); + l->addWidget(bb); + connect(bb, &QDialogButtonBox::accepted, &dlg, &QDialog::accept); + connect(bb, &QDialogButtonBox::rejected, &dlg, &QDialog::reject); + if (dlg.exec() == QDialog::Accepted && lw->currentRow() >= 0 && lw->currentRow() < matrixList.size()) + matrixEdit->setPlainText(formatMatrixText(matrixList[lw->currentRow()].second)); + return; + } + + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + QMessageBox::critical(this, "错误", QString("无法打开文件: %1").arg(fileName)); + return; + } + QTextStream in(&file); + QString text = in.readAll(); + file.close(); + QMatrix4x4 matrix; + QString errorMessage; + if (!parseMatrixText(text, matrix, &errorMessage)) { + QMessageBox::warning(this, "格式错误", errorMessage); + return; + } + matrixEdit->setPlainText(formatMatrixText(matrix)); + }); + layout->addWidget(btnLoad); + + // 机械臂位姿输入 + QGroupBox* poseGroup = new QGroupBox("机械臂位姿", &dialog); + QGridLayout* poseLayout = new QGridLayout(poseGroup); + poseLayout->setHorizontalSpacing(8); + poseLayout->setVerticalSpacing(6); + + auto addPoseEdit = [&](const QString& label, int row, int col) -> QLineEdit* { + QLabel* lbl = new QLabel(label, poseGroup); + QLineEdit* edit = new QLineEdit("0.0", poseGroup); + edit->setAlignment(Qt::AlignRight); + edit->setFont(QFont("Consolas", 10)); + poseLayout->addWidget(lbl, row, col * 2); + poseLayout->addWidget(edit, row, col * 2 + 1); + return edit; + }; + + QLineEdit* editX = addPoseEdit("X:", 0, 0); + QLineEdit* editY = addPoseEdit("Y:", 0, 1); + QLineEdit* editZ = addPoseEdit("Z:", 0, 2); + QLineEdit* editRx = addPoseEdit("RX:", 1, 0); + QLineEdit* editRy = addPoseEdit("RY:", 1, 1); + QLineEdit* editRz = addPoseEdit("RZ:", 1, 2); + layout->addWidget(poseGroup); + + QDialogButtonBox* btnBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); + btnBox->button(QDialogButtonBox::Ok)->setText("应用转换"); + btnBox->button(QDialogButtonBox::Ok)->setStyleSheet("QPushButton { background-color: #1976D2; color: white; font-weight: bold; }" + "QPushButton:hover { background-color: #1565C0; }"); + layout->addWidget(btnBox); + connect(btnBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + connect(btnBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + + if (dialog.exec() != QDialog::Accepted) return; + + QMatrix4x4 handEyeMatrix; + QString errorMessage; + if (!parseMatrixText(matrixEdit->toPlainText().trimmed(), handEyeMatrix, &errorMessage)) { + QMessageBox::warning(this, "格式错误", QString("手眼矩阵无效:%1").arg(errorMessage)); + return; + } + + bool ok = false; + float vals[6]; + const char* labels[] = {"X", "Y", "Z", "RX", "RY", "RZ"}; + QLineEdit* edits[] = {editX, editY, editZ, editRx, editRy, editRz}; + for (int i = 0; i < 6; ++i) { + vals[i] = edits[i]->text().trimmed().toFloat(&ok); + if (!ok) { + QMessageBox::warning(this, "格式错误", QString("机械臂 %1 位姿无效").arg(labels[i])); + return; + } + } + + const QMatrix4x4 totalMatrix = buildRobotPoseMatrix(vals[0], vals[1], vals[2], vals[3], vals[4], vals[5]) * handEyeMatrix; + if (totalMatrix.isIdentity()) { + QMessageBox::information(this, "提示", "当前位姿和手眼矩阵组合为单位矩阵,无需变换"); + return; + } + + applyTransformToAllClouds(totalMatrix); + statusBar()->showMessage( + QString("已应用眼在手上转换,机械臂位姿: [%1, %2, %3, %4, %5, %6]") + .arg(vals[0], 0, 'f', 3).arg(vals[1], 0, 'f', 3).arg(vals[2], 0, 'f', 3) + .arg(vals[3], 0, 'f', 3).arg(vals[4], 0, 'f', 3).arg(vals[5], 0, 'f', 3)); + LOG_INFO("[CloudView] Applied eye-in-hand transform, robot pose=(%.3f, %.3f, %.3f, %.3f, %.3f, %.3f)\n", + vals[0], vals[1], vals[2], vals[3], vals[4], vals[5]); +} + +void CloudViewMainWindow::onSavePointCloud() +{ + if (m_glWidget->getCloudCount() == 0) { + QMessageBox::warning(this, "提示", "请先加载点云"); + return; + } + + const QString fileName = QFileDialog::getSaveFileName( + this, "保存 TXT 点云", QString(), "文本文件 (*.txt);;所有文件 (*.*)"); + if (fileName.isEmpty()) return; + + std::vector> scanLines; + const size_t totalPoints = m_glWidget->getAllCloudsByLines(scanLines); + if (scanLines.empty() || totalPoints == 0) { + QMessageBox::warning(this, "提示", "当前无可保存的点云数据"); + return; + } + + // 进度对话框 + QProgressDialog progress("正在保存点云...", "取消", 0, 100, this); + progress.setWindowTitle("保存点云"); + progress.setWindowModality(Qt::WindowModal); + progress.setMinimumDuration(0); + progress.setAutoClose(true); + progress.setAutoReset(true); + + // 回调实现 + class SaveProgressCallback : public ISaveProgressCallback + { + public: + SaveProgressCallback(QProgressDialog& dlg) : m_dlg(dlg) {} + void onSaveProgress(float progress) override + { + m_dlg.setValue(static_cast(progress * 100)); + QCoreApplication::processEvents(); + } + bool isSaveCancelled() const override + { + return m_dlg.wasCanceled(); + } + private: + QProgressDialog& m_dlg; + }; + + SaveProgressCallback callback(progress); + + LaserDataLoader loader; + const int ret = loader.DebugSaveLaser(fileName.toStdString(), scanLines, &callback); + progress.setValue(100); + + if (ret != 0) { + if (callback.isSaveCancelled()) { + statusBar()->showMessage("保存已取消"); + } else { + QMessageBox::critical(this, "保存失败", + QString("保存点云失败: %1").arg(QString::fromStdString(loader.GetLastError()))); + } + return; + } + + statusBar()->showMessage( + QString("已保存点云: %1 条线, %2 个点 -> %3") + .arg(scanLines.size()).arg(totalPoints).arg(QFileInfo(fileName).fileName())); + LOG_INFO("[CloudView] Saved %zu lines, %zu points to %s\n", + scanLines.size(), totalPoints, fileName.toStdString().c_str()); +} + +void CloudViewMainWindow::onSavePlyPcdCloud() +{ + if (m_glWidget->getCloudCount() == 0) { + QMessageBox::warning(this, "提示", "请先加载点云"); + return; + } + + const QString fileName = QFileDialog::getSaveFileName( + this, "保存 PLY/PCD 点云", QString(), + "PLY 文件 (*.ply);;PCD 文件 (*.pcd);;所有文件 (*.*)"); + if (fileName.isEmpty()) return; + + // 获取所有显示的点云,展平为 PointCloudXYZ(跳过零点) + std::vector> scanLines; + const size_t totalPoints = m_glWidget->getAllCloudsByLines(scanLines); + if (scanLines.empty() || totalPoints == 0) { + QMessageBox::warning(this, "提示", "当前无可保存的点云数据"); + return; + } + + PointCloudXYZ cloud; + const float EPS = 1e-6f; + for (const auto& line : scanLines) { + for (const auto& pt : line) { + // 跳过零点(网格填充的空位) + if (std::fabs(pt.pt3D.x) < EPS && + std::fabs(pt.pt3D.y) < EPS && + std::fabs(pt.pt3D.z) < EPS) + continue; + cloud.points.emplace_back(pt.pt3D.x, pt.pt3D.y, pt.pt3D.z); + } + } + + if (cloud.empty()) { + QMessageBox::warning(this, "提示", "当前无可保存的有效点云数据"); + return; + } + + // 根据扩展名选择保存格式 + const std::string path = fileName.toStdString(); + std::string ext; + const auto dotPos = path.rfind('.'); + if (dotPos != std::string::npos) + ext = path.substr(dotPos + 1); + for (auto& c : ext) c = static_cast(std::tolower(c)); + + int ret = -1; + if (ext == "ply") { + ret = m_converter->saveToPly(path, cloud); + } else if (ext == "pcd") { + ret = m_converter->saveToPcd(path, cloud); + } else { + // 用户手动输入了其他扩展名,默认用 PLY + ret = m_converter->saveToPly(path, cloud); + } + + if (ret != 0) { + QMessageBox::critical(this, "保存失败", + QString("保存点云失败: %1") + .arg(QString::fromStdString(m_converter->getLastError()))); + return; + } + + statusBar()->showMessage( + QString("已保存点云: %1 个点 -> %2") + .arg(cloud.points.size()).arg(QFileInfo(fileName).fileName())); + LOG_INFO("[CloudView] Saved %zu points to %s\n", + cloud.points.size(), fileName.toStdString().c_str()); +} + +void CloudViewMainWindow::onBatchConvertTxtToPly() +{ + // 选择源文件夹 + const QString srcDir = QFileDialog::getExistingDirectory( + this, "选择源文件夹(包含 txt 点云文件)", QString(), + QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + if (srcDir.isEmpty()) return; + + // 选择目标文件夹 + const QString dstDir = QFileDialog::getExistingDirectory( + this, "选择目标文件夹(PLY 文件输出位置)", srcDir, + QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + if (dstDir.isEmpty()) return; + + // 递归收集所有 .txt 文件及其相对路径 + struct FileEntry + { + QString absolutePath; + QString relativePath; // 相对于源文件夹的路径,用于构建目标路径 + }; + QVector fileEntries; + + { + QDirIterator it(srcDir, QStringList("*.txt"), QDir::Files, + QDirIterator::Subdirectories | QDirIterator::FollowSymlinks); + while (it.hasNext()) { + it.next(); + FileEntry entry; + entry.absolutePath = it.filePath(); + // 计算相对路径(文件路径去掉源目录前缀) + entry.relativePath = QDir(srcDir).relativeFilePath(it.filePath()); + fileEntries.append(entry); + } + } + + if (fileEntries.isEmpty()) { + QMessageBox::information(this, "提示", + QString("源文件夹及其子目录中没有找到 .txt 文件:\n%1").arg(srcDir)); + return; + } + + // 按相对路径排序 + std::sort(fileEntries.begin(), fileEntries.end(), + [](const FileEntry& a, const FileEntry& b) { + return a.relativePath < b.relativePath; + }); + + // 进度对话框 + QProgressDialog progress("正在批量转换 txt -> ply...", "取消", 0, fileEntries.size(), this); + progress.setWindowTitle("批量转换"); + progress.setWindowModality(Qt::WindowModal); + progress.setMinimumDuration(0); + progress.setAutoClose(true); + + // 使用独立的转换器,避免干扰主窗口状态 + PointCloudConverter converter; + int successCount = 0; + int failCount = 0; + int skipCount = 0; + QStringList failedFiles; + + const float kZeroEps = 1e-6f; + + for (int i = 0; i < fileEntries.size(); ++i) { + if (progress.wasCanceled()) { + statusBar()->showMessage("批量转换已取消"); + break; + } + + const FileEntry& entry = fileEntries[i]; + const QString& srcPath = entry.absolutePath; + const QString& relPath = entry.relativePath; + + // 构建目标路径:目标文件夹 + 相对路径,扩展名改为 .ply + const QString baseName = QFileInfo(relPath).completeBaseName(); + const QString relDir = QFileInfo(relPath).path(); // 相对路径中的目录部分 + const QString dstRelPath = (relDir == "." ? baseName + ".ply" + : relDir + "/" + baseName + ".ply"); + const QString dstPath = dstDir + "/" + dstRelPath; + + // 确保目标子目录存在 + const QString dstParentDir = QFileInfo(dstPath).absolutePath(); + if (!QDir().mkpath(dstParentDir)) { + failCount++; + failedFiles.append(QString("%1 (无法创建目标目录)").arg(relPath)); + LOG_WARN("[CloudView] Batch convert: failed to create dir %s\n", + dstParentDir.toStdString().c_str()); + continue; + } + + // 显示简短的相对路径便于阅读 + const QString displayPath = relPath.length() > 60 + ? ("..." + relPath.right(57)) : relPath; + + progress.setLabelText(QString("正在转换: %1 (%2/%3)") + .arg(displayPath).arg(i + 1).arg(fileEntries.size())); + progress.setValue(i); + QCoreApplication::processEvents(); + + // 加载 txt 点云 + PointCloudXYZ cloud; + int loadRet = converter.loadFromTxt(srcPath.toStdString(), cloud); + if (loadRet != 0) { + failCount++; + failedFiles.append(QString("%1 (加载失败: %2)") + .arg(relPath) + .arg(QString::fromStdString(converter.getLastError()))); + LOG_WARN("[CloudView] Batch convert: failed to load %s\n", + srcPath.toStdString().c_str()); + continue; + } + + // 过滤掉零点,构建用于保存的点云 + PointCloudXYZ filteredCloud; + filteredCloud.reserve(cloud.size()); + for (const auto& pt : cloud.points) { + if (std::fabs(pt.x) < kZeroEps && + std::fabs(pt.y) < kZeroEps && + std::fabs(pt.z) < kZeroEps) + continue; + filteredCloud.push_back(pt); + } + + if (filteredCloud.empty()) { + failCount++; + failedFiles.append(QString("%1 (无有效点)").arg(relPath)); + LOG_WARN("[CloudView] Batch convert: no valid points in %s\n", + srcPath.toStdString().c_str()); + continue; + } + + // 保存为 PLY + int saveRet = converter.saveToPly(dstPath.toStdString(), filteredCloud); + if (saveRet != 0) { + failCount++; + failedFiles.append(QString("%1 (保存失败: %2)") + .arg(relPath) + .arg(QString::fromStdString(converter.getLastError()))); + LOG_WARN("[CloudView] Batch convert: failed to save %s\n", + dstPath.toStdString().c_str()); + continue; + } + + successCount++; + LOG_INFO("[CloudView] Batch convert: %s -> %s (%zu points)\n", + srcPath.toStdString().c_str(), + dstPath.toStdString().c_str(), + filteredCloud.size()); + } + + progress.setValue(fileEntries.size()); + + // 汇总结果 + const int totalCount = fileEntries.size(); + QString summary = QString("批量转换完成!\n\n" + "总文件数: %1\n" + "成功: %2\n" + "失败: %3") + .arg(totalCount) + .arg(successCount) + .arg(failCount); + if (skipCount > 0) { + summary += QString("\n跳过: %1").arg(skipCount); + } + + if (!failedFiles.isEmpty()) { + summary += "\n\n失败文件:\n"; + // 最多显示前 10 个失败文件 + const int maxShow = std::min(failedFiles.size(), 10); + for (int i = 0; i < maxShow; ++i) { + summary += QString(" • %1\n").arg(failedFiles[i]); + } + if (failedFiles.size() > maxShow) { + summary += QString(" ... 还有 %1 个\n") + .arg(failedFiles.size() - maxShow); + } + } + + QMessageBox::information(this, "转换结果", summary); + statusBar()->showMessage( + QString("批量转换完成: %1 成功, %2 失败").arg(successCount).arg(failCount)); +} + +void CloudViewMainWindow::onRotateCloudByZ() +{ + if (m_glWidget->getCloudCount() == 0) { + QMessageBox::warning(this, "提示", "请先加载点云"); + return; + } + + QDialog dialog(this); + dialog.setWindowTitle("点云旋转"); + dialog.setModal(true); + + QVBoxLayout* layout = new QVBoxLayout(&dialog); + + QLabel* tip = new QLabel("对 Z 值小于阈值的点绕指定轴旋转指定角度。", &dialog); + tip->setWordWrap(true); + tip->setStyleSheet("color: gray;"); + layout->addWidget(tip); + + auto makeSpinBox = [&](double minValue, double maxValue, double value, double step = 1.0) { + QDoubleSpinBox* spin = new QDoubleSpinBox(&dialog); + spin->setDecimals(3); + spin->setRange(minValue, maxValue); + spin->setValue(value); + spin->setSingleStep(step); + spin->setAlignment(Qt::AlignRight); + return spin; + }; + + QGroupBox* thresholdGroup = new QGroupBox("Z 轴阈值", &dialog); + QFormLayout* thresholdLayout = new QFormLayout(thresholdGroup); + QDoubleSpinBox* zThreshold = makeSpinBox(-100000.0, 100000.0, 0.0, 10.0); + thresholdLayout->addRow("Z < ", zThreshold); + layout->addWidget(thresholdGroup); + + QGroupBox* rotateGroup = new QGroupBox("旋转角度 (度)", &dialog); + QFormLayout* rotateLayout = new QFormLayout(rotateGroup); + QDoubleSpinBox* rotX = makeSpinBox(-360.0, 360.0, 0.0, 5.0); + QDoubleSpinBox* rotY = makeSpinBox(-360.0, 360.0, 0.0, 5.0); + QDoubleSpinBox* rotZ = makeSpinBox(-360.0, 360.0, 0.0, 5.0); + rotateLayout->addRow("绕 X 轴:", rotX); + rotateLayout->addRow("绕 Y 轴:", rotY); + rotateLayout->addRow("绕 Z 轴:", rotZ); + layout->addWidget(rotateGroup); + + QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); + connect(buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + layout->addWidget(buttonBox); + + if (dialog.exec() != QDialog::Accepted) { + return; + } + + // 构建旋转矩阵: Rz * Ry * Rx + const QMatrix4x4 rotMatrix = + makeAxisRotationMatrix('z', rotZ->value() * kDegToRad) * + makeAxisRotationMatrix('y', rotY->value() * kDegToRad) * + makeAxisRotationMatrix('x', rotX->value() * kDegToRad); + + const size_t count = m_glWidget->rotateCloudsByZThreshold(rotMatrix, static_cast(zThreshold->value())); + if (count == 0) { + QMessageBox::information(this, "提示", "没有满足条件的点 (Z < 阈值)"); + return; + } + + statusBar()->showMessage( + QString("已旋转 %1 个点 (Z < %2, 绕X %3° Y %4° Z %5°)") + .arg(count) + .arg(zThreshold->value(), 0, 'f', 3) + .arg(rotX->value(), 0, 'f', 1) + .arg(rotY->value(), 0, 'f', 1) + .arg(rotZ->value(), 0, 'f', 1)); + LOG_INFO("[CloudView] Rotated %zu points with Z < %.3f by (%.1f, %.1f, %.1f) deg\n", + count, zThreshold->value(), rotX->value(), rotY->value(), rotZ->value()); +} + +void CloudViewMainWindow::onGeneratePointCloud() +{ + QDialog dialog(this); + dialog.setWindowTitle("生成长方体点云"); + dialog.setModal(true); + + QVBoxLayout* layout = new QVBoxLayout(&dialog); + + QLabel* tip = new QLabel("根据中心点和 XYZ 尺寸生成轴对齐长方体表面点云。", &dialog); + tip->setWordWrap(true); + tip->setStyleSheet("color: gray;"); + layout->addWidget(tip); + + auto makeSpinBox = [&](double minValue, double maxValue, double value) { + QDoubleSpinBox* spin = new QDoubleSpinBox(&dialog); + spin->setDecimals(3); + spin->setRange(minValue, maxValue); + spin->setValue(value); + spin->setSingleStep(1.0); + spin->setAlignment(Qt::AlignRight); + return spin; + }; + + QGroupBox* centerGroup = new QGroupBox("中心点", &dialog); + QFormLayout* centerLayout = new QFormLayout(centerGroup); + QDoubleSpinBox* centerX = makeSpinBox(-100000.0, 100000.0, 0.0); + QDoubleSpinBox* centerY = makeSpinBox(-100000.0, 100000.0, 0.0); + QDoubleSpinBox* centerZ = makeSpinBox(-100000.0, 100000.0, 0.0); + centerLayout->addRow("X:", centerX); + centerLayout->addRow("Y:", centerY); + centerLayout->addRow("Z:", centerZ); + layout->addWidget(centerGroup); + + QGroupBox* sizeGroup = new QGroupBox("三轴方向大小", &dialog); + QFormLayout* sizeLayout = new QFormLayout(sizeGroup); + QDoubleSpinBox* sizeX = makeSpinBox(0.001, 100000.0, 100.0); + QDoubleSpinBox* sizeY = makeSpinBox(0.001, 100000.0, 100.0); + QDoubleSpinBox* sizeZ = makeSpinBox(0.001, 100000.0, 100.0); + QDoubleSpinBox* spacing = makeSpinBox(0.001, 10000.0, 5.0); + sizeLayout->addRow("X 尺寸:", sizeX); + sizeLayout->addRow("Y 尺寸:", sizeY); + sizeLayout->addRow("Z 尺寸:", sizeZ); + sizeLayout->addRow("点间距:", spacing); + layout->addWidget(sizeGroup); + + QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); + connect(buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + layout->addWidget(buttonBox); + + if (dialog.exec() != QDialog::Accepted) { + return; + } + + const QVector3D center(static_cast(centerX->value()), + static_cast(centerY->value()), + static_cast(centerZ->value())); + const QVector3D size(static_cast(sizeX->value()), + static_cast(sizeY->value()), + static_cast(sizeZ->value())); + const float pointSpacing = static_cast(spacing->value()); + + const int gridX = std::max(1, static_cast(std::ceil(size.x() / pointSpacing))) + 1; + const int gridY = std::max(1, static_cast(std::ceil(size.y() / pointSpacing))) + 1; + const int gridZ = std::max(1, static_cast(std::ceil(size.z() / pointSpacing))) + 1; + const double estimatedGridCount = static_cast(gridX) * gridY * gridZ; + if (estimatedGridCount > 3000000.0) { + QMessageBox::warning(this, "提示", "点数预计过大,请增大点间距或减小尺寸。"); + return; + } + + const PointCloudXYZ cloud = generateBoxPointCloud(center, size, pointSpacing); + if (cloud.empty()) { + QMessageBox::warning(this, "提示", "生成点云失败,结果为空。"); + return; + } + + addGeneratedCloud(cloud, QString("GeneratedBox_%1x%2x%3") + .arg(size.x(), 0, 'f', 1) + .arg(size.y(), 0, 'f', 1) + .arg(size.z(), 0, 'f', 1)); +} + +void CloudViewMainWindow::onConvertEulerMatrix() +{ + QDialog* dialog = new QDialog(this); + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->setWindowTitle("欧拉角与方向向量矩阵互转"); + dialog->setWindowFlags(dialog->windowFlags() | Qt::Tool); + + QVBoxLayout* layout = new QVBoxLayout(dialog); + + QLabel* tip = new QLabel( + "输入欧拉角(单位: 度)按所选旋转顺序得到 3x3 方向向量矩阵;也可以从矩阵反算欧拉角。\n" + "方向向量矩阵采用列向量约定: R = [ X轴 | Y轴 | Z轴 ],每列为该坐标轴在参考系中的单位方向向量。", + dialog); + tip->setWordWrap(true); + tip->setStyleSheet("color: gray;"); + layout->addWidget(tip); + + QGroupBox* eulerGroup = new QGroupBox("欧拉角 (deg)", dialog); + QGridLayout* eulerLayout = new QGridLayout(eulerGroup); + + auto makeAngleSpin = [](QWidget* parent) { + QDoubleSpinBox* spin = new QDoubleSpinBox(parent); + spin->setDecimals(6); + spin->setRange(-3600.0, 3600.0); + spin->setSingleStep(0.1); + spin->setValue(0.0); + spin->setAlignment(Qt::AlignRight); + spin->setButtonSymbols(QAbstractSpinBox::NoButtons); + return spin; + }; + + QDoubleSpinBox* eulerRx = makeAngleSpin(eulerGroup); + QDoubleSpinBox* eulerRy = makeAngleSpin(eulerGroup); + QDoubleSpinBox* eulerRz = makeAngleSpin(eulerGroup); + eulerLayout->addWidget(new QLabel("RX:", eulerGroup), 0, 0); + eulerLayout->addWidget(eulerRx, 0, 1); + eulerLayout->addWidget(new QLabel("RY:", eulerGroup), 1, 0); + eulerLayout->addWidget(eulerRy, 1, 1); + eulerLayout->addWidget(new QLabel("RZ:", eulerGroup), 2, 0); + eulerLayout->addWidget(eulerRz, 2, 1); + + QComboBox* comboOrder = new QComboBox(eulerGroup); + comboOrder->addItem("ZYX (Yaw-Pitch-Roll)", static_cast(EulerRotationOrder::ZYX)); + comboOrder->addItem("XYZ (Roll-Pitch-Yaw)", static_cast(EulerRotationOrder::XYZ)); + comboOrder->addItem("ZXY (Yaw-Roll-Pitch)", static_cast(EulerRotationOrder::ZXY)); + comboOrder->addItem("YXZ (Pitch-Roll-Yaw)", static_cast(EulerRotationOrder::YXZ)); + comboOrder->addItem("XZY (Roll-Yaw-Pitch)", static_cast(EulerRotationOrder::XZY)); + comboOrder->addItem("YZX (Pitch-Yaw-Roll)", static_cast(EulerRotationOrder::YZX)); + if (m_comboEulerOrder && m_comboEulerOrder->currentIndex() >= 0) { + comboOrder->setCurrentIndex(m_comboEulerOrder->currentIndex()); + } + eulerLayout->addWidget(new QLabel("旋转顺序:", eulerGroup), 3, 0); + eulerLayout->addWidget(comboOrder, 3, 1); + + QGroupBox* matrixGroup = new QGroupBox("方向向量矩阵 (3x3)", dialog); + QGridLayout* matLayout = new QGridLayout(matrixGroup); + + auto makeHeader = [matrixGroup](const QString& text) { + QLabel* label = new QLabel(text, matrixGroup); + label->setAlignment(Qt::AlignCenter); + label->setStyleSheet("font-weight: bold;"); + return label; + }; + + matLayout->addWidget(makeHeader("X轴"), 0, 1); + matLayout->addWidget(makeHeader("Y轴"), 0, 2); + matLayout->addWidget(makeHeader("Z轴"), 0, 3); + matLayout->addWidget(new QLabel("X分量:", matrixGroup), 1, 0); + matLayout->addWidget(new QLabel("Y分量:", matrixGroup), 2, 0); + matLayout->addWidget(new QLabel("Z分量:", matrixGroup), 3, 0); + + std::array, 3> matCells{}; + for (int c = 0; c < 3; ++c) { + for (int r = 0; r < 3; ++r) { + QDoubleSpinBox* spin = new QDoubleSpinBox(matrixGroup); + spin->setDecimals(6); + spin->setRange(-1.0, 1.0); + spin->setSingleStep(0.01); + spin->setAlignment(Qt::AlignRight); + spin->setButtonSymbols(QAbstractSpinBox::NoButtons); + spin->setValue(r == c ? 1.0 : 0.0); + matCells[r][c] = spin; + matLayout->addWidget(spin, r + 1, c + 1); + } + } + + layout->addWidget(eulerGroup); + layout->addWidget(matrixGroup); + + QHBoxLayout* actionLayout = new QHBoxLayout(); + QPushButton* eulerToMat = new QPushButton("欧拉角 -> 方向向量矩阵", dialog); + QPushButton* matToEuler = new QPushButton("方向向量矩阵 -> 欧拉角", dialog); + QPushButton* closeButton = new QPushButton("关闭", dialog); + actionLayout->addWidget(eulerToMat); + actionLayout->addWidget(matToEuler); + actionLayout->addWidget(closeButton); + layout->addLayout(actionLayout); + + connect(eulerToMat, &QPushButton::clicked, dialog, [=]() { + const EulerRotationOrder order = static_cast(comboOrder->currentData().toInt()); + const QMatrix4x4 rotation = buildEulerRotationMatrix(order, + eulerRx->value(), + eulerRy->value(), + eulerRz->value()); + for (int r = 0; r < 3; ++r) { + for (int c = 0; c < 3; ++c) { + matCells[r][c]->setValue(rotation(r, c)); + } + } + }); + + connect(matToEuler, &QPushButton::clicked, dialog, [=]() { + QMatrix4x4 rotation; + rotation.setToIdentity(); + for (int r = 0; r < 3; ++r) { + for (int c = 0; c < 3; ++c) { + rotation(r, c) = static_cast(matCells[r][c]->value()); + } + } + const EulerRotationOrder order = static_cast(comboOrder->currentData().toInt()); + const QVector3D euler = rotationMatrixToEuler(order, rotation); + eulerRx->setValue(euler.x()); + eulerRy->setValue(euler.y()); + eulerRz->setValue(euler.z()); + }); + + connect(closeButton, &QPushButton::clicked, dialog, &QDialog::close); + dialog->show(); +} + +void CloudViewMainWindow::onPoseCompensation() +{ + QDialog* dialog = new QDialog(this); + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->setWindowTitle("姿态补偿计算"); + dialog->setWindowFlags(dialog->windowFlags() | Qt::Tool); + + QVBoxLayout* layout = new QVBoxLayout(dialog); + + QLabel* tip = new QLabel( + "输入当前欧拉角和期望欧拉角(单位: 度),计算出补偿角。\n" + "分量相减:补偿 = 期望 − 当前(仅小角度差/单轴旋转时近似有效,结果与顺序无关)\n" + "矩阵补偿:补偿 = 提取(R_cur^T × R_des)\n" + " · 输入顺序:当前/期望欧拉角的旋转顺序(取决于机器人/算法侧约定)\n" + " · 补偿输出顺序:反解 R_comp 用的旋转顺序,要与下游施加补偿的约定一致\n" + " (DetectPresenter::ApplyAxesRotation 硬编码 R = Rx·Ry·Rz,即 XYZ 内旋)", + dialog); + tip->setWordWrap(true); + tip->setStyleSheet("color: gray;"); + layout->addWidget(tip); + + auto fillEulerOrderItems = [](QComboBox* combo) { + combo->addItem("ZYX (Yaw-Pitch-Roll)", static_cast(EulerRotationOrder::ZYX)); + combo->addItem("XYZ (Roll-Pitch-Yaw)", static_cast(EulerRotationOrder::XYZ)); + combo->addItem("ZXY (Yaw-Roll-Pitch)", static_cast(EulerRotationOrder::ZXY)); + combo->addItem("YXZ (Pitch-Roll-Yaw)", static_cast(EulerRotationOrder::YXZ)); + combo->addItem("XZY (Roll-Yaw-Pitch)", static_cast(EulerRotationOrder::XZY)); + combo->addItem("YZX (Pitch-Yaw-Roll)", static_cast(EulerRotationOrder::YZX)); + }; + + QComboBox* comboInputOrder = new QComboBox(dialog); + fillEulerOrderItems(comboInputOrder); + if (m_comboEulerOrder && m_comboEulerOrder->currentIndex() >= 0) { + comboInputOrder->setCurrentIndex(m_comboEulerOrder->currentIndex()); + } + + QComboBox* comboOutputOrder = new QComboBox(dialog); + fillEulerOrderItems(comboOutputOrder); + // 默认 XYZ,匹配 DetectPresenter::ApplyAxesRotation 中 R = Rx·Ry·Rz 的硬编码 + const int outputXyzIndex = comboOutputOrder->findData(static_cast(EulerRotationOrder::XYZ)); + if (outputXyzIndex >= 0) { + comboOutputOrder->setCurrentIndex(outputXyzIndex); + } + + QHBoxLayout* orderLayout = new QHBoxLayout(); + orderLayout->addWidget(new QLabel("输入顺序:", dialog)); + orderLayout->addWidget(comboInputOrder); + orderLayout->addSpacing(20); + orderLayout->addWidget(new QLabel("补偿输出顺序:", dialog)); + orderLayout->addWidget(comboOutputOrder); + orderLayout->addStretch(); + layout->addLayout(orderLayout); + + QComboBox* comboMethod = new QComboBox(dialog); + comboMethod->addItem("分量相减(期望 − 当前)"); + comboMethod->addItem("矩阵补偿(R_cur^T × R_des)"); + + QHBoxLayout* methodLayout = new QHBoxLayout(); + methodLayout->addWidget(new QLabel("计算方法:", dialog)); + methodLayout->addWidget(comboMethod); + methodLayout->addStretch(); + layout->addLayout(methodLayout); + + auto makeAngleSpin = [](QWidget* parent) { + QDoubleSpinBox* spin = new QDoubleSpinBox(parent); + spin->setDecimals(6); + spin->setRange(-3600.0, 3600.0); + spin->setSingleStep(0.1); + spin->setValue(0.0); + spin->setAlignment(Qt::AlignRight); + spin->setButtonSymbols(QAbstractSpinBox::NoButtons); + return spin; + }; + + QGroupBox* currentGroup = new QGroupBox("当前欧拉角 (deg)", dialog); + QGridLayout* currentLayout = new QGridLayout(currentGroup); + QDoubleSpinBox* curRx = makeAngleSpin(currentGroup); + QDoubleSpinBox* curRy = makeAngleSpin(currentGroup); + QDoubleSpinBox* curRz = makeAngleSpin(currentGroup); + currentLayout->addWidget(new QLabel("RX:", currentGroup), 0, 0); + currentLayout->addWidget(curRx, 0, 1); + currentLayout->addWidget(new QLabel("RY:", currentGroup), 1, 0); + currentLayout->addWidget(curRy, 1, 1); + currentLayout->addWidget(new QLabel("RZ:", currentGroup), 2, 0); + currentLayout->addWidget(curRz, 2, 1); + + QGroupBox* desiredGroup = new QGroupBox("期望欧拉角 (deg)", dialog); + QGridLayout* desiredLayout = new QGridLayout(desiredGroup); + QDoubleSpinBox* desRx = makeAngleSpin(desiredGroup); + QDoubleSpinBox* desRy = makeAngleSpin(desiredGroup); + QDoubleSpinBox* desRz = makeAngleSpin(desiredGroup); + desiredLayout->addWidget(new QLabel("RX:", desiredGroup), 0, 0); + desiredLayout->addWidget(desRx, 0, 1); + desiredLayout->addWidget(new QLabel("RY:", desiredGroup), 1, 0); + desiredLayout->addWidget(desRy, 1, 1); + desiredLayout->addWidget(new QLabel("RZ:", desiredGroup), 2, 0); + desiredLayout->addWidget(desRz, 2, 1); + + QGroupBox* resultGroup = new QGroupBox("补偿欧拉角 (deg)", dialog); + QGridLayout* resultLayout = new QGridLayout(resultGroup); + QLineEdit* resultRx = new QLineEdit(resultGroup); + QLineEdit* resultRy = new QLineEdit(resultGroup); + QLineEdit* resultRz = new QLineEdit(resultGroup); + resultRx->setReadOnly(true); + resultRy->setReadOnly(true); + resultRz->setReadOnly(true); + resultRx->setAlignment(Qt::AlignRight); + resultRy->setAlignment(Qt::AlignRight); + resultRz->setAlignment(Qt::AlignRight); + resultRx->setStyleSheet("background-color: #f0f0f0; font-weight: bold;"); + resultRy->setStyleSheet("background-color: #f0f0f0; font-weight: bold;"); + resultRz->setStyleSheet("background-color: #f0f0f0; font-weight: bold;"); + resultLayout->addWidget(new QLabel("RX:", resultGroup), 0, 0); + resultLayout->addWidget(resultRx, 0, 1); + resultLayout->addWidget(new QLabel("RY:", resultGroup), 1, 0); + resultLayout->addWidget(resultRy, 1, 1); + resultLayout->addWidget(new QLabel("RZ:", resultGroup), 2, 0); + resultLayout->addWidget(resultRz, 2, 1); + + layout->addWidget(currentGroup); + layout->addWidget(desiredGroup); + layout->addWidget(resultGroup); + + QHBoxLayout* actionLayout = new QHBoxLayout(); + QPushButton* calcButton = new QPushButton("计算补偿", dialog); + QPushButton* closeButton = new QPushButton("关闭", dialog); + actionLayout->addStretch(); + actionLayout->addWidget(calcButton); + actionLayout->addWidget(closeButton); + layout->addLayout(actionLayout); + + connect(calcButton, &QPushButton::clicked, dialog, [=]() { + if (comboMethod->currentIndex() == 0) { + // 分量相减 + resultRx->setText(QString::number(desRx->value() - curRx->value(), 'f', 6)); + resultRy->setText(QString::number(desRy->value() - curRy->value(), 'f', 6)); + resultRz->setText(QString::number(desRz->value() - curRz->value(), 'f', 6)); + } else { + // 矩阵补偿 + const EulerRotationOrder inputOrder = static_cast(comboInputOrder->currentData().toInt()); + const EulerRotationOrder outputOrder = static_cast(comboOutputOrder->currentData().toInt()); + + // 输入欧拉角按 inputOrder 还原成旋转矩阵 + const QMatrix4x4 curMat = buildEulerRotationMatrix(inputOrder, curRx->value(), curRy->value(), curRz->value()); + const QMatrix4x4 desMat = buildEulerRotationMatrix(inputOrder, desRx->value(), desRy->value(), desRz->value()); + + // R_des = R_cur * R_comp(工具坐标系补偿,R_comp = R_cur^T * R_des) + const QMatrix4x4 compMat = curMat.transposed() * desMat; + + // 补偿矩阵按 outputOrder 反解(要与下游施加补偿的旋转顺序一致) + const QVector3D euler = rotationMatrixToEuler(outputOrder, compMat); + resultRx->setText(QString::number(euler.x(), 'f', 6)); + resultRy->setText(QString::number(euler.y(), 'f', 6)); + resultRz->setText(QString::number(euler.z(), 'f', 6)); + } + }); + + connect(closeButton, &QPushButton::clicked, dialog, &QDialog::close); + dialog->show(); +} + +void CloudViewMainWindow::onEulerOrderChanged(int index) +{ + if (!m_glWidget) { + return; + } + + EulerRotationOrder order = static_cast(m_comboEulerOrder->itemData(index).toInt()); + m_glWidget->setEulerRotationOrder(order); + + // 如果有姿态点,刷新显示 + m_glWidget->update(); + + QString orderName = m_comboEulerOrder->currentText(); + statusBar()->showMessage(QString("欧拉角旋转顺序已切换为: %1").arg(orderName)); + LOG_INFO("[CloudView] Euler rotation order changed to: %s\n", orderName.toStdString().c_str()); +} + +void CloudViewMainWindow::onViewAnglesChanged(float rotX, float rotY, float rotZ) +{ + // 更新显示的角度值 + m_editRotX->setText(QString::number(rotX, 'f', 1)); + m_editRotY->setText(QString::number(rotY, 'f', 1)); + m_editRotZ->setText(QString::number(rotZ, 'f', 1)); +} + +void CloudViewMainWindow::onPoint1CoordChanged() +{ + // 读取编辑框中的坐标 + bool ok = true; + float x = m_editPoint1X->text().toFloat(&ok); + if (!ok) { + QMessageBox::warning(this, "错误", "点1 X 值无效"); + return; + } + + float y = m_editPoint1Y->text().toFloat(&ok); + if (!ok) { + QMessageBox::warning(this, "错误", "点1 Y 值无效"); + return; + } + + float z = m_editPoint1Z->text().toFloat(&ok); + if (!ok) { + QMessageBox::warning(this, "错误", "点1 Z 值无效"); + return; + } + + // 直接设置选中点坐标(无论是否已有鼠标选点) + m_glWidget->setSelectedPointCoord(0, x, y, z); + updateSelectedPointsDisplay(); + + // 如果启用了测距且有两个点,重新计算距离 + auto updatedPoints = m_glWidget->getSelectedPoints(); + if (m_glWidget->isMeasureDistanceEnabled() && updatedPoints.size() >= 2 && updatedPoints[1].valid) { + const float distance = m_glWidget->calculateDistance(updatedPoints[0], updatedPoints[1]); + updateMeasureResultDisplay(updatedPoints[0], updatedPoints[1], distance, "点1坐标已更新"); + } else { + statusBar()->showMessage(QString("点1坐标已更新为 (%1, %2, %3)").arg(x, 0, 'f', 3).arg(y, 0, 'f', 3).arg(z, 0, 'f', 3)); + } + + LOG_INFO("[CloudView] Point1 coord updated to (%.3f, %.3f, %.3f)\n", x, y, z); +} + +void CloudViewMainWindow::onPoint2CoordChanged() +{ + // 读取编辑框中的坐标 + bool ok = true; + float x = m_editPoint2X->text().toFloat(&ok); + if (!ok) { + QMessageBox::warning(this, "错误", "点2 X 值无效"); + return; + } + + float y = m_editPoint2Y->text().toFloat(&ok); + if (!ok) { + QMessageBox::warning(this, "错误", "点2 Y 值无效"); + return; + } + + float z = m_editPoint2Z->text().toFloat(&ok); + if (!ok) { + QMessageBox::warning(this, "错误", "点2 Z 值无效"); + return; + } + + // 直接设置选中点坐标(无论是否已有鼠标选点) + m_glWidget->setSelectedPointCoord(1, x, y, z); + updateSelectedPointsDisplay(); + + // 如果启用了测距,重新计算距离 + auto updatedPoints = m_glWidget->getSelectedPoints(); + if (m_glWidget->isMeasureDistanceEnabled() && updatedPoints.size() >= 2 && updatedPoints[0].valid) { + const float distance = m_glWidget->calculateDistance(updatedPoints[0], updatedPoints[1]); + updateMeasureResultDisplay(updatedPoints[0], updatedPoints[1], distance, "点2坐标已更新"); + } else { + statusBar()->showMessage(QString("点2坐标已更新为 (%1, %2, %3)").arg(x, 0, 'f', 3).arg(y, 0, 'f', 3).arg(z, 0, 'f', 3)); + } + + LOG_INFO("[CloudView] Point2 coord updated to (%.3f, %.3f, %.3f)\n", x, y, z); +} + diff --git a/CloudView3D/CloudView3D.pro b/CloudView3D/CloudView3D.pro new file mode 100644 index 0000000..514a324 --- /dev/null +++ b/CloudView3D/CloudView3D.pro @@ -0,0 +1,30 @@ +QT += core gui widgets opengl + +CONFIG += c++17 staticlib +TEMPLATE = lib +TARGET = CloudView3D + +win32-msvc { + QMAKE_CXXFLAGS += /utf-8 + QMAKE_CXXFLAGS += /bigobj +} + +INCLUDEPATH += $$PWD/Inc +INCLUDEPATH += $$PWD/../VrUtils/Inc +INCLUDEPATH += $$PWD/../VrCommon/Inc +INCLUDEPATH += $$PWD/../../Device/SDK/VzNLSDK/Inc + +HEADERS += \ + Inc/CloudView3D.h \ + Inc/CloudView3DTypes.h \ + Inc/PointCloudGLWidget.h + +SOURCES += \ + Src/PointCloudGLWidget.cpp + +win32 { + LIBS += -lopengl32 + LIBS += -lglu32 +} + +DEFINES += _USE_MATH_DEFINES diff --git a/CloudView3D/Inc/CloudView3D.h b/CloudView3D/Inc/CloudView3D.h new file mode 100644 index 0000000..f0fd68b --- /dev/null +++ b/CloudView3D/Inc/CloudView3D.h @@ -0,0 +1,7 @@ +#ifndef CLOUD_VIEW_3D_H +#define CLOUD_VIEW_3D_H + +#include "CloudView3DTypes.h" +#include "PointCloudGLWidget.h" + +#endif // CLOUD_VIEW_3D_H diff --git a/CloudView3D/Inc/CloudView3DTypes.h b/CloudView3D/Inc/CloudView3DTypes.h new file mode 100644 index 0000000..0cdecb6 --- /dev/null +++ b/CloudView3D/Inc/CloudView3DTypes.h @@ -0,0 +1,54 @@ +#ifndef CLOUD_VIEW_3D_TYPES_H +#define CLOUD_VIEW_3D_TYPES_H + +#include +#include +#include + +struct Point3D +{ + float x, y, z; + Point3D() : x(0), y(0), z(0) {} + Point3D(float _x, float _y, float _z) : x(_x), y(_y), z(_z) {} +}; + +struct Point3DRGB +{ + float x, y, z; + uint8_t r, g, b; + float pointSize; + + Point3DRGB() : x(0), y(0), z(0), r(255), g(255), b(255), pointSize(0) {} + Point3DRGB(float _x, + float _y, + float _z, + uint8_t _r = 255, + uint8_t _g = 255, + uint8_t _b = 255, + float _ps = 0) + : x(_x), y(_y), z(_z), r(_r), g(_g), b(_b), pointSize(_ps) {} +}; + +template +class SimplePointCloud +{ +public: + std::vector points; + std::vector lineIndices; + + void clear() { points.clear(); lineIndices.clear(); } + size_t size() const { return points.size(); } + bool empty() const { return points.empty(); } + void reserve(size_t n) { points.reserve(n); lineIndices.reserve(n); } + void push_back(const PointT& pt) { points.push_back(pt); } + void push_back(const PointT& pt, int lineIdx) + { + points.push_back(pt); + lineIndices.push_back(lineIdx); + } +}; + +using PointCloudXYZ = SimplePointCloud; +using PointCloudXYZRGB = SimplePointCloud; + +#endif // CLOUD_VIEW_3D_TYPES_H diff --git a/CloudView3D/Inc/PointCloudGLWidget.h b/CloudView3D/Inc/PointCloudGLWidget.h new file mode 100644 index 0000000..f974fa4 --- /dev/null +++ b/CloudView3D/Inc/PointCloudGLWidget.h @@ -0,0 +1,521 @@ +#ifndef POINT_CLOUD_GL_WIDGET_H +#define POINT_CLOUD_GL_WIDGET_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "CloudView3DTypes.h" +#include "VZNL_Types.h" + +/** + * @brief 点云显示颜色枚举 + */ +enum class PointCloudColor +{ + White, + Red, + Green, + Blue, + Yellow, + Cyan, + Magenta, + Original +}; + +/** + * @brief 选中的点信息 + */ +struct SelectedPointInfo +{ + bool valid; + size_t index; + float x, y, z; + int cloudIndex; + int lineIndex; // 所属线索引 + int pointIndexInLine; // 点在线中的索引 + + SelectedPointInfo() : valid(false), index(0), x(0), y(0), z(0), cloudIndex(-1), lineIndex(-1), pointIndexInLine(-1) {} +}; + +/** + * @brief 选线模式 + */ +enum class LineSelectMode +{ + Vertical, // 纵向选线(同一条扫描线) + Horizontal // 横向选线(所有线的相同索引点) +}; + +/** + * @brief 欧拉角旋转顺序 + */ +enum class EulerRotationOrder +{ + ZYX, // Yaw-Pitch-Roll(最常用) + XYZ, // Roll-Pitch-Yaw + ZXY, // Yaw-Roll-Pitch + YXZ, // Pitch-Roll-Yaw + XZY, // Roll-Yaw-Pitch + YZX // Pitch-Yaw-Roll +}; + +/** + * @brief 选中的线信息 + */ +struct SelectedLineInfo +{ + bool valid; + int cloudIndex; // 点云索引 + int lineIndex; // 线索引(纵向选线时使用) + int pointIndex; // 点索引(横向选线时使用) + int pointCount; // 该线上的点数 + LineSelectMode mode; // 选线模式 + + SelectedLineInfo() : valid(false), cloudIndex(-1), lineIndex(-1), pointIndex(-1), pointCount(0), mode(LineSelectMode::Vertical) {} +}; + +/** + * @brief 线段数据 + */ +struct LineSegment +{ + float x1, y1, z1; // 起点 + float x2, y2, z2; // 终点 + float r, g, b; // 颜色 (0-1) + float lineWidth; // 线宽,0 表示使用默认值(来自 RGBA 中 A > 1 的值) + + LineSegment() : x1(0), y1(0), z1(0), x2(0), y2(0), z2(0), r(1), g(1), b(1), lineWidth(0) {} + LineSegment(float _x1, float _y1, float _z1, float _x2, float _y2, float _z2, float _r = 1.0f, float _g = 1.0f, float _b = 1.0f, float _lw = 0) + : x1(_x1), y1(_y1), z1(_z1), x2(_x2), y2(_y2), z2(_z2), r(_r), g(_g), b(_b), lineWidth(_lw) {} +}; + +/** + * @brief 面片数据 + */ +struct SurfaceQuad +{ + QVector3D p1, p2, p3, p4; + float r, g, b, a; + + SurfaceQuad() : p1(), p2(), p3(), p4(), r(0.0f), g(0.8f), b(1.0f), a(0.5f) {} + SurfaceQuad(const QVector3D& _p1, + const QVector3D& _p2, + const QVector3D& _p3, + const QVector3D& _p4, + float _r = 0.0f, + float _g = 0.8f, + float _b = 1.0f, + float _a = 0.5f) + : p1(_p1), p2(_p2), p3(_p3), p4(_p4), r(_r), g(_g), b(_b), a(_a) {} +}; + +struct BasicShapePoint +{ + float x, y, z; + float r, g, b; + float pointSize; + + BasicShapePoint() : x(0), y(0), z(0), r(1), g(1), b(1), pointSize(8.0f) {} + BasicShapePoint(float _x, + float _y, + float _z, + float _r = 1.0f, + float _g = 1.0f, + float _b = 1.0f, + float _size = 8.0f) + : x(_x), y(_y), z(_z), r(_r), g(_g), b(_b), pointSize(_size) {} +}; + +/** + * @brief 姿态点数据 + * + * 坐标系约定(右手坐标系): + * - X轴:红色,指向右 + * - Y轴:绿色,指向上 + * - Z轴:蓝色,指向观察者 + * + * 欧拉角旋转顺序:ZYX(Yaw-Pitch-Roll) + * - rz: 绕Z轴旋转(偏航角,Yaw) + * - ry: 绕Y轴旋转(俯仰角,Pitch) + * - rx: 绕X轴旋转(滚转角,Roll) + */ +struct PosePoint +{ + float x, y, z; // 位置 + float rx, ry, rz; // 欧拉角(度) + float scale; // 坐标系大小 + + PosePoint() : x(0), y(0), z(0), rx(0), ry(0), rz(0), scale(10.0f) {} + PosePoint(float _x, float _y, float _z, float _rx, float _ry, float _rz, float _scale = 10.0f) + : x(_x), y(_y), z(_z), rx(_rx), ry(_ry), rz(_rz), scale(_scale) {} +}; + +/** + * @brief 点云 OpenGL 渲染控件 + */ +class PointCloudGLWidget : public QOpenGLWidget, protected QOpenGLFunctions +{ + Q_OBJECT + +public: + explicit PointCloudGLWidget(QWidget* parent = nullptr); + ~PointCloudGLWidget() override; + + void addPointCloud(const PointCloudXYZ& cloud, const QString& name = ""); + void addPointCloud(const PointCloudXYZRGB& cloud, const QString& name = ""); + void clearPointClouds(); + void setPointCloudColor(PointCloudColor color); + void setPointSize(float size); + void resetView(); + void fitToContent(); + void zoomIn(); + void zoomOut(); + /** + * @brief 设置视角旋转角度(不改变缩放和平移) + * @param rotX X轴旋转角度(俯仰) + * @param rotY Y轴旋转角度(偏航) + * @param rotZ Z轴旋转角度(滚转) + */ + void setViewAngles(float rotX, float rotY, float rotZ = 0.0f); + + QVector getSelectedPoints() const { return m_selectedPoints; } + SelectedLineInfo getSelectedLine() const { return m_selectedLine; } + void clearSelectedPoints(); + void clearSelectedLine(); + float calculateDistance(const SelectedPointInfo& p1, const SelectedPointInfo& p2); + + /** + * @brief 更新选中点的坐标 + * @param index 点索引(0或1) + * @param x 新的X坐标 + * @param y 新的Y坐标 + * @param z 新的Z坐标 + */ + void updateSelectedPointCoord(int index, float x, float y, float z); + + /** + * @brief 设置选中点(如果不存在则创建) + * @param index 点索引(0或1) + * @param x X坐标 + * @param y Y坐标 + * @param z Z坐标 + */ + void setSelectedPointCoord(int index, float x, float y, float z); + + /** + * @brief 通过线索引选择线(纵向) + */ + bool selectLineByIndex(int lineIndex); + + /** + * @brief 通过点索引选择横向线(所有线的相同索引点) + */ + bool selectHorizontalLineByIndex(int pointIndex); + + /** + * @brief 设置选线模式 + */ + void setLineSelectMode(LineSelectMode mode) { m_lineSelectMode = mode; } + + /** + * @brief 设置欧拉角旋转顺序 + */ + void setEulerRotationOrder(EulerRotationOrder order) { m_eulerRotationOrder = order; } + + /** + * @brief 获取欧拉角旋转顺序 + */ + EulerRotationOrder getEulerRotationOrder() const { return m_eulerRotationOrder; } + + /** + * @brief 设置是否启用测距功能 + */ + void setMeasureDistanceEnabled(bool enabled) { m_measureDistanceEnabled = enabled; } + + /** + * @brief 获取测距功能是否启用 + */ + bool isMeasureDistanceEnabled() const { return m_measureDistanceEnabled; } + + /** + * @brief 获取选中线上的所有点坐标 + * @return 点坐标列表,每个元素为 (x, y, z) + */ + QVector getSelectedLinePoints() const; + + /** + * @brief 设置列表高亮点(与选点功能区分) + * @param point 点坐标,如果为空则清除高亮 + */ + void setListHighlightPoint(const QVector3D& point); + + /** + * @brief 清除列表高亮点 + */ + void clearListHighlightPoint(); + + /** + * @brief 获取第一个点云的数据(用于旋转和保存) + */ + bool getFirstCloudData(PointCloudXYZ& cloud) const; + + /** + * @brief 替换第一个点云(用于显示旋转后的数据) + */ + void replaceFirstCloud(const PointCloudXYZ& cloud, const QString& name); + + /** + * @brief 获取点云数量 + */ + size_t getCloudCount() const { return m_pointClouds.size(); } + + /** + * @brief 对所有点云应用 4x4 变换矩阵 + * @param matrix 4x4 变换矩阵 + */ + void transformAllClouds(const QMatrix4x4& matrix); + + /** + * @brief 对所有点云中 Z 值小于阈值的点应用旋转矩阵 + * @param rotMatrix 旋转矩阵 + * @param zThreshold Z 轴阈值 + * @return 被旋转的点数 + */ + size_t rotateCloudsByZThreshold(const QMatrix4x4& rotMatrix, float zThreshold); + + /** + * @brief 获取所有点云数据(按线分组),用于保存 + * @param scanLines 输出:每条扫描线的点列表 + * @return 总点数 + */ + size_t getAllCloudsByLines(std::vector>& scanLines) const; + + /** + * @brief 添加线段 + */ + void addLineSegments(const QVector& segments); + + /** + * @brief 清除所有线段 + */ + void clearLineSegments(); + + /** + * @brief 设置基础图形线段 + */ + void setBasicShapeSegments(const QVector& segments); + + /** + * @brief 追加基础图形线段 + */ + void appendBasicShapeSegments(const QVector& segments); + + /** + * @brief 追加基础图形面片 + */ + void appendBasicShapeSurfaces(const QVector& surfaces); + void appendBasicShapePoints(const QVector& points); + + /** + * @brief 清除基础图形线段 + */ + void clearBasicShapeSegments(); + + /** + * @brief 添加姿态点 + */ + void addPosePoints(const QVector& poses); + + /** + * @brief 清除所有姿态点 + */ + void clearPosePoints(); + +signals: + void pointSelected(const SelectedPointInfo& point); + void twoPointsSelected(const SelectedPointInfo& p1, const SelectedPointInfo& p2, float distance); + void measurementFinished(const SelectedPointInfo& p1, + const SelectedPointInfo& p2, + float distance, + float dx, + float dy, + float dz); + void lineSelected(const SelectedLineInfo& line); + void viewAnglesChanged(float rotX, float rotY, float rotZ); + +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 mouseReleaseEvent(QMouseEvent* event) override; + void wheelEvent(QWheelEvent* event) override; + void mouseDoubleClickEvent(QMouseEvent* event) override; + void keyPressEvent(QKeyEvent* event) override; + +private: + struct PointCloudData + { + std::vector vertices; + std::vector colors; + std::vector lineIndices; // 每个点所属的线索引 + std::vector originalIndices; // 每个显示点在原始点云中的索引(用于计算原始index) + std::vector pointInLineIndices; // 每个显示点在所属线中的索引(预计算,支持不等长线) + bool hasColor; + bool hasLineInfo; // 是否有线信息 + QString name; + int colorIndex; // 点云颜色索引 + int totalLines; // 总线数 + int pointsPerLine; // 每线点数(网格化点云) + + // 自定义点大小分组(RGBA 中 A > 1 的点按大小分组) + struct PointSizeGroup { + float pointSize; // 点大小 + std::vector indices; // 该大小的点在顶点数组中的索引(顶点索引 = indices[i] * 3) + }; + std::vector customPointSizeGroups; + bool hasCustomPointSizes; + + // VBO 缓冲区 + QOpenGLBuffer vertexBuffer; // 顶点 VBO + QOpenGLBuffer colorBuffer; // 颜色 VBO + bool vboCreated; // VBO 是否已创建 + + PointCloudData() + : hasColor(false), hasLineInfo(false), colorIndex(0) + , totalLines(0), pointsPerLine(0) + , hasCustomPointSizes(false) + , vertexBuffer(QOpenGLBuffer::VertexBuffer) + , colorBuffer(QOpenGLBuffer::VertexBuffer) + , vboCreated(false) + {} + + // 移动构造函数(QOpenGLBuffer 不支持拷贝) + PointCloudData(PointCloudData&& other) noexcept + : vertices(std::move(other.vertices)) + , colors(std::move(other.colors)) + , lineIndices(std::move(other.lineIndices)) + , originalIndices(std::move(other.originalIndices)) + , pointInLineIndices(std::move(other.pointInLineIndices)) + , hasColor(other.hasColor) + , hasLineInfo(other.hasLineInfo) + , name(std::move(other.name)) + , colorIndex(other.colorIndex) + , totalLines(other.totalLines) + , pointsPerLine(other.pointsPerLine) + , customPointSizeGroups(std::move(other.customPointSizeGroups)) + , hasCustomPointSizes(other.hasCustomPointSizes) + , vertexBuffer(QOpenGLBuffer::VertexBuffer) + , colorBuffer(QOpenGLBuffer::VertexBuffer) + , vboCreated(false) + { + // VBO 需要在 GL 上下文中重建,标记为未创建 + other.vboCreated = false; + } + + PointCloudData& operator=(PointCloudData&& other) noexcept + { + if (this != &other) { + // 释放自身 VBO + if (vboCreated) { + vertexBuffer.destroy(); + colorBuffer.destroy(); + vboCreated = false; + } + vertices = std::move(other.vertices); + colors = std::move(other.colors); + lineIndices = std::move(other.lineIndices); + originalIndices = std::move(other.originalIndices); + pointInLineIndices = std::move(other.pointInLineIndices); + hasColor = other.hasColor; + hasLineInfo = other.hasLineInfo; + name = std::move(other.name); + colorIndex = other.colorIndex; + totalLines = other.totalLines; + pointsPerLine = other.pointsPerLine; + customPointSizeGroups = std::move(other.customPointSizeGroups); + hasCustomPointSizes = other.hasCustomPointSizes; + // VBO 需要重建 + other.vboCreated = false; + } + return *this; + } + + // 禁止拷贝 + PointCloudData(const PointCloudData&) = delete; + PointCloudData& operator=(const PointCloudData&) = delete; + }; + + void computeBoundingBox(); + void setCurrentColor(PointCloudColor color); + void setColorByIndex(int colorIndex); // 根据索引设置颜色 + SelectedPointInfo pickPoint(int screenX, int screenY); + void drawSelectedPoints(); + void drawMeasurementLine(); + void drawAxis(); + void drawSelectedLine(); // 绘制选中的线 + void drawLineSegments(); // 绘制线段 + void drawBasicShapeSurfaces(); // 绘制基础图形面片 + void drawBasicShapeSegments(); // 绘制基础图形 + void drawBasicShapePoints(); + void drawLineSegmentList(const QVector& segments); + void drawPosePoints(); // 绘制姿态点 + void drawAxisLabels(); // 绘制坐标轴 XYZ 标注 + void uploadToVBO(PointCloudData& data); // 上传数据到 VBO + void releaseVBO(PointCloudData& data); // 释放 VBO 资源 + + std::vector m_pointClouds; + + QMatrix4x4 m_projection; + QMatrix4x4 m_view; + QMatrix4x4 m_model; + + float m_distance; + float m_rotationX; + float m_rotationY; + float m_rotationZ; + QQuaternion m_rotation; // 使用四元数存储旋转状态 + QVector3D m_center; + QVector3D m_pan; + QVector3D m_minBound; + QVector3D m_maxBound; + + QPoint m_lastMousePos; + bool m_leftButtonPressed; + bool m_rightButtonPressed; + bool m_middleButtonPressed; + + PointCloudColor m_currentColor; + float m_pointSize; + + LineSelectMode m_lineSelectMode; + EulerRotationOrder m_eulerRotationOrder; + bool m_measureDistanceEnabled; + QVector m_selectedPoints; + SelectedLineInfo m_selectedLine; + static const int MAX_SELECTED_POINTS = 2; + + // 列表高亮点(与选点功能区分) + bool m_hasListHighlightPoint; + QVector3D m_listHighlightPoint; + + int m_colorIndex; // 颜色轮换索引 + static const int COLOR_COUNT = 7; // 可用颜色数量 + + // 线段和姿态点数据 + QVector m_lineSegments; + QVector m_basicShapeSegments; + QVector m_basicShapeSurfaces; + QVector m_basicShapePoints; + QVector m_posePoints; +}; + +#endif // POINT_CLOUD_GL_WIDGET_H diff --git a/CloudView/Src/PointCloudGLWidget.cpp b/CloudView3D/Src/PointCloudGLWidget.cpp similarity index 94% rename from CloudView/Src/PointCloudGLWidget.cpp rename to CloudView3D/Src/PointCloudGLWidget.cpp index 735efb4..fb56b81 100644 --- a/CloudView/Src/PointCloudGLWidget.cpp +++ b/CloudView3D/Src/PointCloudGLWidget.cpp @@ -230,6 +230,7 @@ void PointCloudGLWidget::paintGL() drawLineSegments(); drawBasicShapeSurfaces(); drawBasicShapeSegments(); + drawBasicShapePoints(); drawPosePoints(); // 最后绘制坐标系指示器(覆盖在所有内容之上) @@ -454,6 +455,7 @@ void PointCloudGLWidget::clearPointClouds() m_lineSegments.clear(); m_basicShapeSegments.clear(); m_basicShapeSurfaces.clear(); + m_basicShapePoints.clear(); m_posePoints.clear(); m_minBound = QVector3D(-50, -50, -50); m_maxBound = QVector3D(50, 50, 50); @@ -897,6 +899,10 @@ void PointCloudGLWidget::computeBoundingBox() includePoint(surface.p4.x(), surface.p4.y(), surface.p4.z()); } + for (const BasicShapePoint& point : m_basicShapePoints) { + includePoint(point.x, point.y, point.z); + } + for (const PosePoint& pose : m_posePoints) { const float scale = qMax(0.0f, pose.scale); includePoint(pose.x - scale, pose.y - scale, pose.z - scale); @@ -1290,10 +1296,14 @@ void PointCloudGLWidget::mousePressEvent(QMouseEvent* event) emit pointSelected(point); - if (m_selectedPoints.size() == 2) { - float distance = calculateDistance(m_selectedPoints[0], m_selectedPoints[1]); - emit twoPointsSelected(m_selectedPoints[0], m_selectedPoints[1], distance); - } + if (m_selectedPoints.size() == 2) { + const float distance = calculateDistance(m_selectedPoints[0], m_selectedPoints[1]); + const float dx = m_selectedPoints[1].x - m_selectedPoints[0].x; + const float dy = m_selectedPoints[1].y - m_selectedPoints[0].y; + const float dz = m_selectedPoints[1].z - m_selectedPoints[0].z; + emit twoPointsSelected(m_selectedPoints[0], m_selectedPoints[1], distance); + emit measurementFinished(m_selectedPoints[0], m_selectedPoints[1], distance, dx, dy, dz); + } } else { // 未启用测距:只保留一个点 m_selectedPoints.clear(); @@ -1374,10 +1384,14 @@ void PointCloudGLWidget::mouseDoubleClickEvent(QMouseEvent* event) emit pointSelected(point); - if (m_selectedPoints.size() == 2) { - float distance = calculateDistance(m_selectedPoints[0], m_selectedPoints[1]); - emit twoPointsSelected(m_selectedPoints[0], m_selectedPoints[1], distance); - } + if (m_selectedPoints.size() == 2) { + const float distance = calculateDistance(m_selectedPoints[0], m_selectedPoints[1]); + const float dx = m_selectedPoints[1].x - m_selectedPoints[0].x; + const float dy = m_selectedPoints[1].y - m_selectedPoints[0].y; + const float dz = m_selectedPoints[1].z - m_selectedPoints[0].z; + emit twoPointsSelected(m_selectedPoints[0], m_selectedPoints[1], distance); + emit measurementFinished(m_selectedPoints[0], m_selectedPoints[1], distance, dx, dy, dz); + } } else { // 未启用测距:只保留一个点 m_selectedPoints.clear(); @@ -1607,10 +1621,23 @@ void PointCloudGLWidget::appendBasicShapeSurfaces(const QVector& su update(); } +void PointCloudGLWidget::appendBasicShapePoints(const QVector& points) +{ + if (points.isEmpty()) { + return; + } + + for (const BasicShapePoint& point : points) { + m_basicShapePoints.append(point); + } + update(); +} + void PointCloudGLWidget::clearBasicShapeSegments() { m_basicShapeSegments.clear(); m_basicShapeSurfaces.clear(); + m_basicShapePoints.clear(); update(); } @@ -1659,11 +1686,29 @@ void PointCloudGLWidget::drawBasicShapeSegments() { drawLineSegmentList(m_basicShapeSegments); } - -void PointCloudGLWidget::drawLineSegmentList(const QVector& segments) -{ - if (segments.isEmpty()) { - return; + +void PointCloudGLWidget::drawBasicShapePoints() +{ + if (m_basicShapePoints.isEmpty()) { + return; + } + + glDisable(GL_DEPTH_TEST); + for (const BasicShapePoint& point : m_basicShapePoints) { + glPointSize(point.pointSize); + glBegin(GL_POINTS); + glColor3f(point.r, point.g, point.b); + glVertex3f(point.x, point.y, point.z); + glEnd(); + } + glEnable(GL_DEPTH_TEST); + glPointSize(m_pointSize); +} + +void PointCloudGLWidget::drawLineSegmentList(const QVector& segments) +{ + if (segments.isEmpty()) { + return; } // 按线宽分组绘制 diff --git a/DataUtils/CloudMathClac/CloudMathClac.pro b/DataUtils/CloudMathClac/CloudMathClac.pro index 0b258a3..198ef34 100644 --- a/DataUtils/CloudMathClac/CloudMathClac.pro +++ b/DataUtils/CloudMathClac/CloudMathClac.pro @@ -5,7 +5,7 @@ CONFIG -= qt TARGET = CloudMathClac INCLUDEPATH += $$PWD/Inc -INCLUDEPATH += $$PWD/../../../SDK/Device/VzNLSDK/Inc +INCLUDEPATH += $$PWD/../../../Device/SDK/VzNLSDK/Inc INCLUDEPATH += $$PWD/../../../SDK/eigen-3.3.9 HEADERS += \ @@ -14,7 +14,7 @@ HEADERS += \ SOURCES += \ Src/CurveFitting.cpp -# 平台特定配置 +# 骞冲彴鐗瑰畾閰嶇疆 unix { QMAKE_CXXFLAGS += -fPIC } diff --git a/Utils.pro b/Utils.pro index b19555f..3478236 100644 --- a/Utils.pro +++ b/Utils.pro @@ -3,9 +3,10 @@ TEMPLATE = subdirs # 定义子项目 VrCommon.file = VrCommon/VrCommon.pro VrUtils.file = VrUtils/VrUtils.pro -CloudUtils.file = CloudUtils/CloudUtils.pro -DataUtils.file = DataUtils/DataUtils.pro -CloudView.file = CloudView/CloudView.pro +CloudUtils.file = CloudUtils/CloudUtils.pro +DataUtils.file = DataUtils/DataUtils.pro +CloudView3D.file = CloudView3D/CloudView3D.pro +CloudView.file = CloudView/CloudView.pro # 添加子项目 SUBDIRS += \ @@ -14,12 +15,15 @@ SUBDIRS += \ CloudUtils \ DataUtils -win32-msvc { - SUBDIRS += CloudView -} +win32-msvc { + SUBDIRS += \ + CloudView3D \ + CloudView +} # 设置依赖关系 VrUtils.depends = VrCommon CloudUtils.depends = VrCommon VrUtils -DataUtils.depends = VrCommon -CloudView.depends = VrCommon VrUtils CloudUtils +DataUtils.depends = VrCommon +CloudView3D.depends = VrCommon VrUtils +CloudView.depends = VrCommon VrUtils CloudUtils CloudView3D diff --git a/VrCommon/Inc/VZNL_Types.h b/VrCommon/Inc/VZNL_Types.h index 231ce9c..0049d41 100644 --- a/VrCommon/Inc/VZNL_Types.h +++ b/VrCommon/Inc/VZNL_Types.h @@ -1,2328 +1,2328 @@ -/* - * Header: vizum_type.h - * Description:当前文件为双目相机(Vizum)研发的EyeCB板提供所有类型的定义。 - * Author: Mjw - * Date: 2018/08/28 - * Update: 2023/04/27 - */ - -#ifndef __VIZUM_TYPES_HEADER__ -#define __VIZUM_TYPES_HEADER__ - -/// @name 网络相关定义 -/// @{ -/// @brief -/// 网络IP长度 -#define VZNL_SDK_NETWORK_IPv4_LENGTH 16 - -/// @brief -/// 网络MAC长度 -#define VZNL_SDK_NETWORK_MAC_LENGTH 18 -/// @} - -/// @breif -/// 版本字符串长度 -#define VZNL_VERSION_LENGTH 64 - -/// @brief -/// 设备名称长度。 -#define VZNL_DEVICE_NAME_LENGTH 64 - -/// @brief -/// 唯一标识符大小 -#define VZNL_GUID_LENGTH 64 - -/// @brief -/// 设备名称长度 -#define VZ_DEVICE_NAME 64 - -/// @brief -/// 设备描述长度 -#define VZ_DEVICE_DESC 256 - -/// @brief -/// 设备ID长度 -#define VZNL_DEVICEID_LENGTH 64 - -/// @brief -/// 设备地址 -#define VZNL_DEVICE_ADDR_LENGTH 64 - -/// @brief -/// File Path Length -#define VZ_MAXPATH 256 - -/// @brief UserProfile MAX Count -#define VZ_USERPROFILE_MAX_COUNT 10 - -#define VZ_USERCONFIG_MAX_COUNT 200 - -/// @brief UserProfile Name Length -#define VZ_USERPROFILE_NAME_LENGTH 64 - -#define VZ_USERCONFIG_NAME_LENGTH 64 - -#define VZ_USERCONFIG_DESC_LENGTH 256 - - -/// @brief -/// 设备句柄 -typedef void* VZNLHANDLE; - -/// @brief -/// 设备句柄 -typedef void* VZNLFILE; - -/// @brief -/// 检测工具句柄 SDK 2.2.0开始支持 -typedef void* VZNLDETECTHANDLE; - -/// @brief -/// 数据处理句柄 -typedef void* VZNLPOINTCLOUDHANDLE; - -/// @brief -/// 数据处理句柄 -typedef void* VZNLIMAGEGENERATOR; - -/// @brief -/// RGB点云处理工具句柄 -typedef void* VZNLRGBCLOUDPOINTTOOL; - -enum -{ - VzFalse = 0, - VzTrue = 1, -}; -typedef unsigned char VzBool; - -/// @brief -/// 颜色定义 -typedef unsigned int VzColorRGB; //RGB - -/// @brief -/// Data Type -#ifdef _WIN64 - typedef unsigned long long VzParam; -#else - typedef unsigned long VzParam; -#endif - -typedef struct -{ - unsigned short shYear; - unsigned short shMonth; - unsigned short shDay; - unsigned short shHour; - unsigned short shMinute; - unsigned short shSecond; - unsigned short shWeek; -} SVzDateTime; - -// 设备搜索标志 -enum -{ - keSearchDeviceFlagNone = 0x0000, //< 不搜索 - keSearchDeviceFlag_EyeCB = 0x0001, //< 搜索控制器 - keSearchDeviceFlag_USBLargeEye = 0x0002, //< 搜索USB极光眼 - keSearchDeviceFlag_EthLargeEye = 0x0004, //< 搜索网络星光眼:激光 - keSearchDeviceFlag_EthSmallEye = 0x0008, //< 搜索网络星光眼:智能 - keSearchDeviceFlag_EthLaserRobotEye = 0x0010, //< 搜索网络智光眼:激光 - keSearchDeviceFlagAll = keSearchDeviceFlag_EthLaserRobotEye, //< 搜索全部支持的设备 -}; -typedef unsigned int EVzSearchDeviceFlag; - -/// @brief -/// 眼睛分辨率枚举值 -typedef enum -{ - keVzNLEyeResolution_Unknown = 0, - keVzNLEyeResolution_130WH, ///< 130W像素的眼睛 1280 * 960 - keVzNLEyeResolution_130WV, ///< 130W像素的眼睛 960 * 1280 - keVzNLEyeResolution_300WH, ///< 300W像素的眼睛 2048 * 1520 - keVzNLEyeResolution_300WV, ///< 300W像素的眼睛 1536 * 2048 - keVzNLEyeResolution_1300W, ///< 1300W像素的眼睛 - keVzNLEyeResolutionLast -} EVzNLEyeResolution; - -/// @brief -/// 设备类型 -typedef enum -{ - keDeviceType_Unknown = 0, - keDeviceType_EyeCB, // EyeCB控制器 - keDeviceType_LaserEye, // 星光眼:激光 - keDeviceType_SmartEye, // 星光眼:智能 - keDeviceType_LaserRobotEye, // 智光眼:激光 - keDeviceType_SmartRobotEye, // 智光眼:智能 -} EVzDeviceType; - -/// @brief -/// 眼睛类型标志 -enum -{ - keEyeTypeFlag_Unknown = 0x00, - keEyeTypeFlag_USB = 0x01, //< USB - keEyeTypeFlag_Eth = 0x02, //< 网络 - - keEyeTypeFlag_Color = 0x10, //< 彩色 - keEyeTypeFlag_Gray = 0x20, //< 黑白 - - keEyeTypeFlag_Laser = 0x100, //< 极光眼 - keEyeTypeFlag_Smart = 0x200, //< 智能眼 - - keEyeTypeFlag_Product1 = 0x1000, //< 小眼睛 - keEyeTypeFlag_Product2 = 0x2000, //< 大眼睛 - keEyeTypeFlag_Product3 = 0x3000, //< 大眼睛 -}; -typedef unsigned int EVzEyeTypeFlag; - -enum -{ - keEyeSensorType_Left = 0x01, - keEyeSensorType_Right = 0x02, -}; -typedef unsigned int EVzEyeSensorType; - -/// @bried -/// 眼睛出图方式 -typedef enum -{ - keEyeImgMode_Low = 1, - keEyeImgMode_Middle, - keEyeImgMode_High, - keEyeImgMode_Higher, - keEyeImgMode_Hights, - keEyeImgMode_Algo -}EVzEyeImgBitMode; - -/// @brief -/// 运动方向 -typedef enum -{ - keObjRunDirect_Plus = 0, //< 正方向 - keObjRunDirect_Minus = 1, //< 反方向 -} EVzObjRunDirect; - -/// @brief -/// 电动机运行方向 -typedef enum -{ - keEncodeMotorRunDirectNone = 0, // 未知 - keEncodeMotorRunDirect_ClockWise, // 顺时针 - keEncodeMotorRunDirect_CounterClockWise, // 逆时针 -} EVzEncodeMotorRunDirect; - -/// @brief -/// 外部触发模式 -enum -{ - keEyeTriggerMode_Invalid = -1, // 无效值 - keEyeTriggerMode_Slave = 0, // 被动触发 - keEyeTriggerMode_Master = 1, // 主动模式 - keEyeTriggerMode_FallingEdge = 2, // 下降沿触发 - keEyeTriggerMode_RisingEdge = 3, // 上升沿触发 - keEyeTriggerMode_LowLevelEnable = 4, // 低电平使能数据输出 - keEyeTriggerMode_ManualTrigger = 6, // 手动模式 - keEyeTriggerMode_Package = 9, // 物流包裹触发模式 - keEyeTriggerMode_HighFrequenceFallingEdge = 10, // 高帧率下降沿触发 -}; -typedef unsigned int EVzEyeTriggerMode; - -/// @brief -/// 设置点云计算模式 -typedef enum -{ - kePointCloudProcMode_Invalid = 0, //< 无效 - kePointCloudProcMode_Speed, //< 速度偏移模式 - kePointCloudProcMode_Encoder, //< 编码器模式 - kePointCloudProcMode_FixedStep, //< 固定间隔模式 -} EVzPointCloudProcMode; - -/// @brief -/// 设备状态 -typedef enum -{ - keDeviceWorkStatusUnknown = 0, //< 未知 - keDeviceWorkStatus_Eye_Comming = 1, //< 眼睛连接 - keDeviceWorkStatus_Eye_Reconnect, //< 眼睛重连 - keDeviceWorkStatus_Working, //< 工作中 - keDeviceWorkStatus_Offline, //< 离线 - keDeviceWorkStatus_Eye_TimeOut, //< 眼睛超时 - keDeviceWorkStatus_Eye_UnStability, //< 眼睛不稳定 - keDeviceWorkStatus_EyeCB_Mem, //< 内存 - keDeviceWorkStatus_EyeCB_TimeStampDiffErr, //< 时间戳错误 - keDeviceWorkStatus_Device_Swing_Finish, //< 摆动模块已完成 - keDeviceWorkStatus_Device_Auto_Stop, //< 设备自动停止 - keDeviceWorkStatus_EyeCB_TaskFull, //< 任务饱和 - keDeviceWorkStatus_EyeCB_GetImageErr, //< 获取图像错误 - - keDeviceWorkStatus_EyeCB_ObjectCome, //< 物体来了 - keDeviceWorkStatus_EyeCB_ObjectLeave, //< 物体走了 - - keDeviceWorkStatus_Trigger_Coming, //< Trigger信号来了 - keDeviceWorkStatus_Trigger_Leave, //< Trigger信号结束了 - - keDeviceWorkStatus_Eye_StreamResume, //< 数据恢复 - keDeviceWorkStatus_Eye_SwingNoResponse, //< 静态模块无响应 - - keDeviceWorkStatus_PetaMsg, //< Peta 消息 - keDeviceWorkStatus_PSMsg, //< PS 消息 - keDeviceWorkStatus_Eye_LaserlineErr, //< - keDeviceWorkStatus_Device_Swing_Running, //< 开流后摆动模块开始运动时给出信号 - - keDeviceWorkStatus_Eye_SwingEncodeNoChange, //< 摆动编码器值不改变 - keDeviceWorkStatus_Eye_SwingUpdateStatus, //< 发现了摆动模块 - - keDeviceWorkStatus_Eye_New_Client_Connected, //< 有新客户端连接 - - keDeviceWorkStatus_Eye_LinearActuator_Warning, //< 推杆电机关闭点可能存在异常 -} EVzDeviceWorkStatus; - -/// @brief -/// 设备状态 -typedef enum -{ - keMulDevWorkStatusUnknown = 0, //< 未知 - keMulDevWorkStatus_Auto_Stop, //< 自动停止 -} EVzMulDevWorkStatus; - -/// @brief -/// Laser FrameRate -typedef enum -{ - keLaserFrameRate_30 = 0, - keLaserFrameRate_100, - keLaserFrameRate_220, - keLaserFrameRate_400, - keLaserFrameRate_630, - keLaserFrameRate_50, - keLaserFrameRate_120, - keLaserFrameRate_320, - keLaserFrameRate_550, - keLaserFrameRate_830, - keLaserFrameRate_80, - keLaserFrameRate_210, - keLaserFrameRate_530, - keLaserFrameRate_840, - keLaserFrameRate_1200, - keLaserFrameRateLast -} EVzLaserFrameRate; - -/// @brief -/// Laser FrameRate -typedef enum -{ - keLaserCaptureMode_LastFrame, - keLaserCaptureMode_AllFrame, - keLaserCaptureMode_CalcAllFrameWhenStop, -} EVzLaserCaptureMode; - -/// @brief -/// 产品类型 -enum -{ - keProjectType_None = 0, - keProjectType_Coal, - keProjectType_Logistics, - keProjectType_InfraredMark, - keProjectType_DivRebar, - keProjectTypeLast, -}; -typedef unsigned int EVzProjectType; - -/// @brief -/// 定义检测物体类型 -enum -{ - keVzNLDetectObjType_Unknown = 0, - keVzNLDetectObjType_Line = 1, // 直线 - keVzNLDetectObjType_CoplaneCircle = 2, // 圆/椭圆(共面) - keVzNLDetectObjType_Triangle = 3, - keVzNLDetectObjType_Rect = 4, // 矩形 - keVzNLDetectObjType_Cube = 5, // 立方体 - keVzNLDetectObjType_StickWidth = 6, // 杆状物 - keVzNLDetectObjType_StickLength = 7, - keVzNLDetectObjType_StickVolume = 8, - keVzNLDetectObjType_StickApex = 9, - keVzNLDetectObjType_Laser = 10, // 激光 - keVzNLDetectObjType_Slot = 11, // 凹槽 - keVzNLDetectObjType_SameSizeCircle = 12, // 同大小 - keVzNLDetectObjType_3DModel = 14, // 匹配三维模型 -}; -typedef unsigned int EVzNLDetectObjType; - -enum -{ - keSDKType_Unknown = 0, - keSDKType_DetectShape, - keSDKType_DetectLaser, - keSDKType_DetectCircle, - keSDKType_DetectColorMark, - keSDKType_DetectGrains, -}; -typedef int EVzSDKType; - -typedef enum -{ - keVzColorType_None = 0, - keVzColorType_Red, - keVzColorType_Green, - keVzColorType_Blue, - keVzColorType_Last, -} EVzColorType; - -/// @brief -/// Log Level -enum -{ - keNLLogLevel_None = 0, - keNLLogLevel_Error, - keNLLogLevel_Warning, - keNLLogLevel_Debug, - keNLLogLevel_Information, -}; -typedef int EVzNLLogLevel; - -/// @brief -/// Log Type -enum -{ - keNLLogType_None = 0, - keNLLogType_Console = 0x01, - keNLLogType_File = 0x02, - keNLLogType_LocalPgm= 0x04, - keNLLogType_Remote = 0x08, -}; -typedef int EVzNLLogType; - -/* -* @brief -* 眼睛配置信息 -*/ -typedef enum -{ - keNLEthernetEyeIPType_Unknown = 0, //< 未知 - keNLEthernetEyeIPType_StaticIP, //< 静态IP - keNLEthernetEyeIPType_DHCP, //< DHCP -} EVzNLEthernetEyeIPType; - -/// @brief -/// 爆光模式 -typedef enum -{ - keVzNLExposeMode_Fix = 0, ///< 定值曝光 - keVzNLExposeMode_Auto = 1, ///< 自动曝光 -} EVzNLExposeMode; - -/// @brief -/// Expose Type -typedef enum -{ - keExposeThresType_Average = 0, - keExposeThresType_Max = 1, -} EVzExposeThresType; - -/// @brief -/// 图像压缩格式 -typedef enum -{ - keVzNLImageCompress_Unknown = 0, - keVzNLImageCompress_Jpeg = 1, // Jpeg格式 - keVzNLImageCompress_Png = 2, // png格式 - keVzNLImageCompress_Bmp = 3 // bmp格式 -} EVzNLImageCompress; - -/// @brief -/// 图像类型 -typedef enum -{ - keVzNLImageType_None = 0, - keVzNLImageType_RGB888, // 3 channel 24bit - keVzNLImageType_BGR888, // 3 channel 24bit - keVzNLImageType_BAYERGR, // 1 channel - keVzNLImageType_BAYERRG, // 1 channel - keVzNLImageType_BAYERBG, // 1 channel - keVzNLImageType_BAYERGB, // 1 channel - keVzNLImageType_GRAY, // 1 channel - keVzNLImageType_YUV420, - keVzNLImageType_BGRA8888, // 4 channel 32bit - keVzNLImageType_YUYV_YUV422,// - keVzNLImageType_UNKNOWN = 0xffff, -} EVzNLImageType; - -/// @brief -/// 圆形定义 -typedef enum -{ - keNL3DCircleTypeUnknown = 0, - keNL3DCircleType_Circle = 3, // 单圆 - keNL3DCircleType_ConcentricCircle = 4, // 同心圆 - keNL3DCircleType_SteelTube = 5, // 钢管 -} EVzNL3DCircleType; - -/// @brief -/// 眼睛数据工作模式 -typedef enum -{ - keNLEyeDataWorkMode_Data = 0, // 数据模式 - keNLEyeDataWorkMode_ImageData = 1, // 图像数据模式 - keNLEyeDataWorkMode_Image = 2, -} EVzNLEyeDataWorkMode; - -/// @breif -/// 数据翻转 -enum -{ - keFlipType_None = 0x00, ///< 无翻转 - keFlipType_Vertical = 0x01, ///< 垂直翻转 -}; -typedef unsigned int EVzFlipType; - -/// @brief -/// 激光点过滤类型 -enum -{ - keLaserPointFilterType_None = 0, ///< 不过滤 - keLaserPointFilterType_BetweenYMinMax = 0x01, ///< 保留Y轴某范围内的点 - keLaserPointFilterType_ExceptYRange = 0x02, ///< 排除Y轴某范围 - keLaserPointFilterType_BetweenZMinMax = 0x10, ///< 保留Z轴某范围的点 - keLaserPointFilterType_ExceptZRange = 0x20, ///< 排除Z轴某范围的点 - keLaserPointFilterType_BetweenXMinMax = 0x100, ///< 保留X轴某范围的点 - keLaserPointFilterType_ExceptXRange = 0x200 ///< 排除X轴某范围的点 -}; - -typedef unsigned int EVzLaserPointFilterType; - -// @brief -// 摆动机构扫描模式 -typedef enum -{ - keSwingMotorScanModeNone = 0, - keSwingMotorScanMode_Once, //< 一次 - keSwingMotorScanMode_Loop, //< 轮询 -} EVzSwingMotorScanMode; - -// @brief -// 灰度值来源 -typedef enum -{ - keGrayValueSource_Left = 0, //< 左目 - keGrayValueSource_Right, //< 右目 - keGrayValueSource_Both, //< 左右 -} EVzGrayValueSource; - -// @brief -// 摆动机构速度 -const float keSwingMotorSpeedMin = 1.f; -const float keSwingMotorSpeed_12 = 12.f; -const float keSwingMotorSpeed_24 = 24.f; -const float keSwingMotorSpeed_36 = 36.f; -const float keSwingMotorSpeed_48 = 48.f; -const float keSwingMotorSpeed_60 = 60.f; -const float keSwingMotorSpeed_72 = 72.f; -const float keSwingMotorSpeed_84 = 84.f; -const float keSwingMotorSpeed_96 = 96.f; -const float keSwingMotorSpeedMax = keSwingMotorSpeed_96; - -// @brief -// 参考ROI大小 -typedef enum -{ - keDeviceROIInvalid = 0, - keDeviceROI_128_1024, //< ROI 128 * 1024 - keDeviceROI_256_1024, //< ROI 256 * 1024 - keDeviceROI_Full, //< ROI Full Resolution - keDeviceROILast //< -} EVzDeviceROI; - -/// @brief -/// 摆动机构旋转方向 -typedef enum -{ - keSwingRotateDirect_Clockwise, //< 顺时针旋转 - keSwingRotateDirect_Counterclockwise //< 逆时针旋转 -} EVzSwingRotateDirect; - -/// @brief -/// 图像分辨率 -typedef struct -{ - unsigned int nFrameWidth; - unsigned int nFrameHeight; -} SVzVideoResolution; - -/// @brief -/// 点类型 -typedef enum -{ - keResultDataType_Invalid = -1, // 3D数据类型 2D数据类型 - keResultDataType_Position, //< Position数据类型 使用SVzNL3DPosition SVzNL2DPosition - keResultDataType_PointF, //< PointF数据类型 使用SVzNL3DPointF SVzNL2DLRPoint - keResultDataType_PointXYZ, //< PointXYZ数据类型 使用SVzNLPointXYZ SVzNL2DLRPoint - keResultDataType_PointXYZRGBA, //< PointXYZRGB数据 使用SVzNLPointXYZRGBA SVzNL2DLRPoint - keResultDataType_PointGray, //< PointXYZGray数据 使用SVzNLPointXYZGray SVzNL2DLRPoint - keResultDataType_PointXYZI, //< PointXYZI数据 使用SVzNLPointXYZI SVzNL2DLRPoint - keResultDataType_PointRGBA_D, //< PointRGBA_D 使用SVzNLPointRGBA_D SVzNL2DLRPoint - keResultDataType_PositionF, //< Position数据类型 使用SVzNL3DPosition SVzNL2DPositionF -} EVzResultDataType; - -/// @brief 设备参数类型 -typedef enum -{ - keDeviceParamType_Invalid = 0, //< 无效 - keDeviceParamType_FrameRate, //< 帧率参数 - keDeviceParamType_Expose, //< 曝光参数 - keDeviceParamType_Gain, //< 增益参数 - keDeviceParamType_LaserLight, //< 激光亮度参数 - keDeviceParamType_FrameRateWithoutColorData,//< 不携带颜色数据的帧率 - keDeviceParamType_FrameRateWithColorData, //< 携带颜色数据的帧率 - keDeviceParamType_StrobeTimeRatio, //< C版激光器亮度值 - keDeviceParamType_RGBExpose, //< RGB曝光参数 - keDeviceParamType_RGBGain, //< RGB增益参数 -} EVzDeviceParamType; - -/// @brief 设备参数类型 -typedef enum -{ - keDevChipInfoInvalid = 0, - keDevChipInfo_Temperature, - keDevChipInfo_VccPin, - keDevChipInfo_VccPaux, - keDevChipInfo_VccPdro, -} EVzDevChipInfo; - -/// @brief -/// 网络信息 -typedef struct -{ - unsigned int nAdapterIndex; //< 网络适配器索引 - char szNetCardName[VZ_MAXPATH]; //< 网卡名称 - unsigned char byLocalIP[4]; //< 本机网卡IP - unsigned char byMacAddress[6]; //< 本机网卡MAC - unsigned char bySubMask[4]; //< 子网掩码 - unsigned char byGetWay[4]; //< 网关 - unsigned int dwSpeed; //< 网络速度 Mbps -} SVzNetCardInfo; - -/// @brief -/// 眼睛控制板的设备信息 -typedef struct -{ - unsigned int nSize; // 结构大小 - EVzDeviceType eDeviceType; // 设备类型 - char szDeviceName[VZNL_DEVICE_NAME_LENGTH]; // 名称 - char byServerIP[VZNL_SDK_NETWORK_IPv4_LENGTH]; // 服务端IP - char byMAC[VZNL_SDK_NETWORK_MAC_LENGTH]; // MAC地址 - char szGUID[VZNL_GUID_LENGTH]; // 唯一标识 - char szDeviceID[VZNL_GUID_LENGTH]; // 设备ID - char szDesc[VZ_DEVICE_DESC]; // 设备描述 - VzBool bValidDevice; // Device是否有效 -} SVzNLEyeCBInfo; - -/// @brief -/// 控制板信息 -typedef struct -{ - SVzNLEyeCBInfo sEyeCBInfo; //< Base Info - unsigned int nHardwareVersion; //< Device HW Version - unsigned int chEyeVersion; //< Device Eye Version - char chEyeName[VZ_DEVICE_NAME]; //< Device Eye Name - char chEyeIPv4[VZNL_SDK_NETWORK_IPv4_LENGTH];//< Device IP Address - char chEyeMAC[VZNL_SDK_NETWORK_MAC_LENGTH]; //< Device Eye MAC - char szDeviceAddr[VZNL_DEVICE_ADDR_LENGTH]; //< Device Address - SVzVideoResolution sVideoRes; //< Device Video Resolution - char chHwSerialID[VZNL_DEVICEID_LENGTH]; //< Device ID Length - -} SVzNLEyeCBDeviceInfoEx; - -/// @brief 设备能力描述 -typedef union -{ - unsigned int nCapabilityMask; - struct - { - unsigned int bIsSupportRGBSensor : 1; //< 是否支持Center Sensor - unsigned int bSupportSwingMotor : 1; //< 是否支持摆动机构 - unsigned int bIsSupportAsyncRGBD : 1; //< 是否支持异步RGBD - }; - -} SVzEthLaserEyeCapability; - -/// @brief -/// 眼睛信息 -typedef struct -{ - SVzNLEyeCBInfo sEyeCBInfo; - EVzEyeTypeFlag eEyeTypeFlag; //< 眼睛类型标志 - SVzVideoResolution sVideoRes; //< 眼睛分辨率 - int nFixDisparity; //< 偏心 - SVzEthLaserEyeCapability sCapability; - unsigned int nMaxFrameIdx; //< 帧号最大值 - unsigned int nMaxTimeStamp; //< 时间戳最大值 - int nHardwareVersion; //< 硬件版本 - char szDeviceAddr[VZNL_DEVICE_ADDR_LENGTH]; //< Device Address - char szVersion[VZNL_VERSION_LENGTH]; //< Version - - // Ethernet Info - EVzNLEthernetEyeIPType eEthernetIPType; //< 以太网IP类型 - char chEyeIP[VZNL_SDK_NETWORK_IPv4_LENGTH]; //< 眼睛IP - char chEyeMac[VZNL_SDK_NETWORK_MAC_LENGTH]; //< 当前眼睛MAC - char chLocalIP[VZNL_SDK_NETWORK_IPv4_LENGTH];//< 网卡IP - char chLocalMac[VZNL_SDK_NETWORK_MAC_LENGTH];//< 网卡MAC - char vendor[8]; //< - char productName[8]; //< 产品名 - char serialID[8]; //< 序列号 - - // Ext Data - unsigned int nSDKClientCnt; //< SDK Link Count - unsigned int nModbusClientCnt; //< Modbus Link Count - unsigned int nEthernetSpeed; //< xxx Mbps - char szHardwareVersion[32]; //< HardWareVersion - unsigned int nUdpFlag; //< 用于判断是否可以使用UDP广播修改IP,0表示不可以 1表示可以 - unsigned int nPupilDistance; //< 瞳距 - - // NetCard Info - SVzNetCardInfo sNetCardInfo; //< 网卡信息 -} SVzNLEyeDeviceInfoEx; - -/// @brief 系统配置参数 -typedef struct -{ - unsigned int nDeviceTimeOut; // 设备超时设置 -} SVzNLConfigParam; - -/// @brief 打开设备类型 -typedef struct _tagSVzNLOpenDeviceParam -{ - EVzNLEyeResolution eEyeResolution; // 眼睛的分辨率 - EVzSDKType eSDKType; - VzBool bStopStream; - _tagSVzNLOpenDeviceParam() - { - eEyeResolution = keVzNLEyeResolution_Unknown; - eSDKType = keSDKType_Unknown; - bStopStream = VzTrue; - } -} SVzNLOpenDeviceParam; - -typedef struct -{ - char szPath[256]; -} SVzNLTestOpenDeviceParam; - -/// @brief -/// 更新工程信息 -typedef struct -{ - char szProject[32]; - char szVersion[32]; -} SVzSystemProjectInfo; - -/// @breif -/// 版本信息。 -typedef struct -{ - long bValidSDKVersion: 1; ///< SDK版本是否可用 - long bValidAppVersion: 1; ///< EyeCB版本是否可用 - long bValidCameraVersion: 1; ///< 眼睛库版本是否可用 - long bValidAlgoVersion: 1; ///< 算法版本是否可用 - long bValidApkVersion: 1; ///< 安卓版本是否可用 - long bValidDevIDVersion: 1; ///< DEVID版本是否可用 - long bValidWifiVersion: 1; ///< WIFI版本是否可用 - long bValidHDMIVersion: 1; ///< HDMI版本是否可用 - long bValidVizumEyeVersion : 1; ///< 硬件版本是否可用 - long bValidFwVersion : 1; ///< 硬件版本是否可用 -} SVzNLVersionCapGroup; - -typedef union -{ - SVzNLVersionCapGroup sVersionCap; - long nMaskCap; -} SVzNLDevVersionCap; - -/// @brief -/// 版本信息 -typedef struct -{ - SVzNLDevVersionCap sCap; - char szSDKVersion[VZNL_VERSION_LENGTH]; ///< SDK版本 - char szAppVersion[VZNL_VERSION_LENGTH]; ///< App版本 - char szCameraVersion[VZNL_VERSION_LENGTH]; ///< 眼睛版本 - char szAlgoVersion[VZNL_VERSION_LENGTH]; ///< 算法版本 - char szApkVersion[VZNL_VERSION_LENGTH]; ///< 安卓版本 - char szDevIDVersion[VZNL_VERSION_LENGTH]; ///< 设备ID版本 - char szWifiVersion[VZNL_VERSION_LENGTH]; ///< WIFI版本 - char szHDMIVersion[VZNL_VERSION_LENGTH]; ///< HDMI版本 - char szVizumEyeVersion[VZNL_VERSION_LENGTH];///< 硬件版本 - char szFwVersion[VZNL_VERSION_LENGTH]; ///< 硬件版本 -} SVzNLVersionInfo; - -/// @brief -/// 图像信息 -typedef struct -{ - unsigned char* pBuffer; // 图像buffer信息 - unsigned int nBufferSize; // 图像buffer的大小 - - unsigned int nWidth; // 图像宽度 - unsigned int nHeight; // 图像高度 - unsigned char nChannels; // 通道数 - EVzNLImageCompress eImageCompress; -} SVzNLCompressData; - -/// @brief -/// 点坐标 -typedef struct -{ - int x; - int y; -} SVzNL2DPoint; - -/// @brief -/// 点坐标 -typedef struct -{ - unsigned short x; - unsigned short y; -} SVzNL2DPointShortU; - -/// @brief -/// 点坐标 -typedef struct -{ - SVzNL2DPoint sLeft; - SVzNL2DPoint sRight; -} SVzNL2DLRPoint; - -/// @brief -/// 点坐标 -typedef struct -{ - float x; - float y; -} SVzNL2DPointF; - -typedef struct -{ - double x; - double y; -} SVzNL2DPointD; - -typedef struct -{ - double x; - double y; - double z; -} SVzNL3DPoint; - -typedef struct -{ - float x; - float y; - float z; -} SVzNL3DPointF; - -// 3D Point -typedef union -{ - float fData[4]; - struct - { - float x; - float y; - float z; - }; -} SVzNLPointXYZ; - -// 3D Point + RGB -typedef union -{ - float fData[4]; - struct - { - float x; - float y; - float z; - VzColorRGB nRGB; - }; -} SVzNLPointXYZRGBA; - -// 3D Point + Gray -typedef union -{ - float fData[4]; - struct - { - float x; - float y; - float z; - int nGray; - }; -} SVzNLPointXYZGray; - -// 3D Point + Gray -typedef struct -{ - union - { - float fData[4]; - struct - { - float x; - float y; - float z; - }; - }; - - union - { - float fData_c[4]; - struct - { - float intensity; - }; - }; -} SVzNLPointXYZI; - -// 3D Point + Gray -typedef struct -{ - union - { - double dData[4]; - struct - { - double x; - double y; - double z; - VzColorRGB nRGB; - }; - }; -} SVzNLPointRGBA_D; - -typedef struct -{ - int x; - int y; - int z; - int flag; -} SVzNLLogisticsPoint; - -typedef struct -{ - float x; - float y; - float z; - float reserve; -} SVzNLAlign3DPointF; - -typedef struct -{ - int x; - int y; - int z; - int reserve; -} SVzNLAlign3DPointI; - -struct SVz3DPointFWithPixel -{ - float x; - float y; - float z; - unsigned char byPixelL; - unsigned char byPixelR; - unsigned char byReserve[2]; -}; - -/// @brief 范围类型 double -typedef struct -{ - float min; //< 最小值 - float max; //< 最大值 -} SVzNLRangeF; - -/// @brief 范围类型 double -typedef struct -{ - double min; //< 最小值 - double max; //< 最大值 -} SVzNLRangeD; - -/// @brief 范围类型 int -typedef struct -{ - unsigned int nMin; //< 最小值 - unsigned int nMax; //< 最大值 -} SVzNLRangeU; - -/// @brief 范围类型 int -typedef struct -{ - int nMin; //< 最小值 - int nMax; //< 最大值 -} SVzNLRange; - -/// @brief 3D值范围 -typedef struct -{ - SVzNLRangeD xRange; //< X范围 - SVzNLRangeD yRange; //< Y范围 - SVzNLRangeD zRange; //< Z范围 -} SVzNL3DRangeD; - -/// @brief 线的2D信息 -typedef struct -{ - SVzNL2DPoint ptStart2D; // 2D起始坐标 - SVzNL2DPoint ptEnd2D; // 2D结束坐标 -} SVzNL2DLine; - -typedef struct -{ - SVzNL2DPointF ptStart2D; // 2D起始坐标 - SVzNL2DPointF ptEnd2D; // 2D结束坐标 -} SVzNL2DLineF; - -/// @brief 线的3D信息 -typedef struct -{ - SVzNL3DPoint ptStart3D; // 3D起始坐标 - SVzNL3DPoint ptEnd3D; // 3D结束坐标 -} SVzNL3DLine; - -/// @brief 2D矩形 -typedef struct -{ - SVzNL2DPoint ptLT; //< 左上 - SVzNL2DPoint ptRT; //< 右上 - SVzNL2DPoint ptRB; //< 右下 - SVzNL2DPoint ptLB; //< 左下 -} SVzNL2DRect; - -/// @brief 3D矩形 -typedef struct -{ - SVzNL3DPoint ptLT; //< 左上 - SVzNL3DPoint ptRT; //< 右上 - SVzNL3DPoint ptRB; //< 右下 - SVzNL3DPoint ptLB; //< 左下 -} SVzNL3DRect; - -/// @brief 三角形 -typedef struct -{ - SVzNL3DPoint ptVertexA; - SVzNL3DPoint ptVertexB; - SVzNL3DPoint ptVertexC; -} SVzNL3DTriangle; - -/// @brief 三角形 -typedef struct -{ - SVzNL2DPoint ptVertexA; - SVzNL2DPoint ptVertexB; - SVzNL2DPoint ptVertexC; -} SVzNL2DTriangle; - -/// @brief 点的坐标 -typedef struct -{ - int nPointIdx; // 点序 - SVzNL2DPoint ptLeft2D; // 2D左图坐标 - SVzNL2DPoint ptRight2D; // 2D右图坐标 - SVzNL3DPoint pt3D; // 3D坐标 -} SVzNLPosition; - -/// @brief 点坐标 (double) -typedef struct -{ - int nPointIdx; // 点序 - SVzNL2DPointD ptLeft2D; // 2D左图坐标 - SVzNL2DPointD ptRight2D; // 2D右图坐标 - SVzNL3DPoint pt3D; // 3D坐标 -} SVzNLPositionD; - -/// @brief 2D点坐标 -typedef struct -{ - int nPointIdx; // 点序 - SVzNL2DPoint ptLeft2D; // 2D左图坐标 - SVzNL2DPoint ptRight2D; // 2D右图坐标 -} SVzNL2DPosition; - -/// @brief 2D点坐标 -typedef struct -{ - int nPointIdx; // 点序 - SVzNL2DPointF ptLeft2D; // 2D左图坐标 - SVzNL2DPointF ptRight2D; // 2D右图坐标 -} SVzNL2DPositionF; - - -/// @brief 3D点坐标 -typedef struct -{ - int nPointIdx; // 点序 - SVzNL3DPoint pt3D; // 3D坐标 -} SVzNL3DPosition; - -/// @brief 3D激光线 -typedef struct -{ - SVzNL3DPosition* p3DPosition; //< 3D点(数组) - int nPositionCnt; //< 3D点个数 - unsigned long long nTimeStamp; //< 时间戳 -} SVzNL3DLaserLine; - -/// @brief 3D激光线 -typedef struct -{ - SVzNLPointXYZRGBA* p3DPoint; //< 3D点(数组) - int nPointCnt; //< 3D点个数 - unsigned long long nTimeStamp; //< 时间戳 -} SVzNLXYZRGBDLaserLine; - -/// @brief 3D点坐标(float) -typedef struct -{ - int nPointIdx; //< 点索引 - SVzNL3DPointF pt3D; //< 点值 -} SVzNL3DPositionF; - -/// @brief 矩形 -typedef struct -{ - int left; //< 左 - int right; //< 右 - int top; //< 上 - int bottom; //< 下 -} SVzNLRect; - -/// @brief 大小 -typedef struct -{ - int nWidth; //< 宽 - int nHeight; //< 高 -} SVzNLSize; - -/// @brief 大小 -typedef struct -{ - short shWidth; //< 宽 - short shHeight; //< 高 -} SVzNLSizeSh; - -/// @brief 大小 -typedef struct -{ - float fWidth; //< 宽 - float fHeight; //< 高 -} SVzNLSizeF; - -/// @brief 大小(double) -typedef struct -{ - double dWidth; //< 宽 - double dHeight; //< 高 -} SVzNLSizeD; - -/// @brief 三角形的类型 -typedef struct -{ - double dSideA; - double dSideB; - double dSideC; -} SVzNLTriangleInfo; - -/// @brief -/// ROI区域 -/// o————————————————o -/// | ↑ ↑ | -/// | |T |B | -/// | ↓ | | -/// | L o——|——o | -/// |←———→| | | | -/// | R | | | | -/// |←————|——↓—→| | -/// | o—————o | -/// o————————————————o -typedef struct -{ - int left; // 左 - int right; // 右 - int top; // 上 - int bottom; // 下 -} SVzNLROIRect; - -/// @brief 2D 位置 -typedef struct -{ - double x; //< X - double y; //< Y - double value; //< None -} SVzNL2DValueD; - -/// @brief 设备范围数据 -typedef struct -{ - SVzNLRangeU sFrameRate; //< 帧率范围 - SVzNLRangeU sExpose; //< 曝光范围 - SVzNLRangeU sGain; //< 增益范围 -} SVzDeviceLimitInfo; - -/// @brief 图像信息 -typedef struct -{ - unsigned char* pBuffer; // 图像buffer信息 - unsigned int nBufferSize; // 图像buffer的大小 - - SVzNL2DPoint ptOriPos; // 原始位置(顶点) - unsigned int nWidth; // 图像宽度 - unsigned int nHeight; // 图像高度 - EVzNLImageType eImageType; // 图像格式 - unsigned char byBitDepth; // 位图深度 - unsigned char nChannels; // 通道数 -} SVzNLImageData; - -//--------------------------------------------------------------- -// @brief 3D数据二维索引表 -//--------------------------------------------------------------- -typedef struct -{ - EVzResultDataType ePointType; // 3D点类型 - void* pPointPtr; // 3D点指针 - unsigned int nWidth; // 二维表宽 - unsigned int nHeight; // 二维表高 -} SVz3DPointMapTable; - -/// @breif 各检测类型的结构体 -typedef struct -{ - EVzNLDetectObjType eDetectType; - int nReserve[2];//< 保留指针,可能是内部的一个对象? -} SVzNLDetectObjectResult; - -/// @breif 线的2D结果 -typedef struct -{ - SVzNL2DLine sLeftLine; - SVzNL2DLine sRightLine; - double dLineLength; // 线长 -} SVzNL2DLineResult; - -/// @breif 线的3D结果 -typedef struct -{ - SVzNL3DLine sLine; - double dLineLength; // 线长 -} SVzNL3DLineResult; - -/// @breif 线的2D结果 -typedef struct -{ - SVzNL2DRect sLeftRect; - SVzNL2DRect sRightRect; - - double dTopDistance; - double dRightDistance; - double dBottomDistance; - double dLeftDistance; -} SVzNL2DRectResult; - -/// @breif 2D结果 -typedef struct -{ - SVzNL3DRect sRect; - double dTopDistance; - double dRightDistance; - double dBottomDistance; - double dLeftDistance; -} SVzNL3DRectResult; - -/// @breif 三角形结果 -typedef struct -{ - SVzNL2DTriangle sLeftTriangle; - SVzNL2DTriangle sRightTriangle; - - double dABDistance; - double dBCDistance; - double dACDistance; -} SVzNL2DTriangleResult; - -typedef struct -{ - SVzNL3DTriangle sTriangle; - - double dABDistance; - double dBCDistance; - double dACDistance; -} SVzNL3DTriangleResult; - -/// @brief 椭圆检测结果 -typedef struct -{ - SVzNLDetectObjectResult sDetectObj; - SVzNL2DPoint s2DPoint; -} SVzNLDetectEllipseResult; - -typedef struct -{ - int nSize; -} SVzNLDetectCubeParam; - -typedef struct -{ - SVzNLDetectObjectResult sDetectObj; -} SVzNLDetectCubeResult; - -/// @brief 3D物体的连接线 -typedef struct -{ - int nLineStart; - int nLineEnd; -} SVzNL3DObjectLine; - -/// @brief 像素深度值归一化模式 -typedef enum -{ - keDepthNormalizingMode_None = 0, //不使用归一化 - keDepthNormalizingMode_Indicated = 1, //指定固定归一化范围 - keDepthNormalizingMode_Auto = 2 //自动检测归一化范围 -} EVzDepthNormalizingMode; - -/// @brief 图像位深 -typedef enum -{ - keImageBitDepthTypeNone = 0, - keImageBitDepthType_8bit = 8, - keImageBitDepthType_16bit = 16, -} EVzImageBitDepthType; - -/// 深度图存储配置参数 -typedef struct -{ - EVzDepthNormalizingMode eDepthNormalizingMode; // 像素深度值归一化模式 - unsigned int nImageMargin; // 输出深度图边缘预留空白区域宽度(单位:像素值) - EVzImageBitDepthType eBitDepth; // 输出深度图的位深度: - float fDepthResolution; // 深度图中深度值精度,例如:0.1 表示1个像素值对应0.1mm 实际距离 - - float fWHPixelResolution; // 深度图中每个像素的精度,例如:0.1 表示1个像素点对应0.1mm实际距离,eDepthNormalizingMode = eDepthNormalizingMode_Indicated 时有效 - float fNormalizingStartVal; // 客户指定的归一化范围的起始值(mm):eDepthNormalizingMode = keDepthNormalizingMode_Indicated 时有效 - float fNormalizingEndVal; // 客户指定的归一化范围的结束值(mm):eDepthNormalizingMode = keDepthNormalizingMode_Indicated 时有效 - - float fNormalizingMargin; // eDepthNormalizingMode = keDepthNormalizingMode_Auto 时,在自动检测到的归一化区间上下预留的空白空间高度(单位mm)上下各预留指定高度空间 - -} SVzDepthMapImgCtlPara; - -/// @brief 3D物体面信息 -typedef struct -{ - int nPointCount; - unsigned int* pArryPointIdx; -} SVzNL3DObjectPanel; - -/// @brief 三维物体描述 -typedef struct -{ - int nType; - SVzNL3DPoint* pGrabPoint; ///< 物体抓取点 - int nGrabPointCount; - SVzNL3DPoint* p3DPoint; ///< 物体各顶点 - int n3DPointCount; ///< 物体上点的个数 - SVzNL3DObjectLine* p3DObjLine; ///< 物体上的线 - int n3DObjLineCount; ///< 物体上线的个数 - SVzNL3DObjectPanel* p3DObjPanel; - int n3DObjPanelCount; -} SVzNL3DObjectDesc; - -/// @brief 三维物体描述 -typedef struct -{ - SVzNL2DPoint* pLeftGrabPoint; ///< 物体抓取点 - SVzNL2DPoint* pRightGrabPoint; ///< 物体抓取点 - int nGrabPointCount; - SVzNL2DPoint* pLeft2DPoint; ///< 物体各顶点 - SVzNL2DPoint* pRight2DPoint; ///< 物体各顶点 - int n2DPointCount; ///< 物体上点的个数 - SVzNL3DObjectLine* p3DObjLine; ///< 物体上的线 - int n3DObjLineCount; ///< 物体上线的个数 - SVzNL3DObjectPanel* p3DObjPanel; - int n3DObjPanelCount; -} SVzNL3DObject2DResult; - -/// @brief ColorMark 形状 -typedef enum -{ - keVzNLColorMarkType_Unknown = 0, - keVzNLColorMarkType_Rect, - keVzNLColorMarkType_Triangle, - keVzNLColorMarkType_Circle, -} EVzNLColorMarkType; - -/// @brief Color Mark -typedef struct -{ - int nPointIdx; // 点序 - SVzNL3DPoint pt3D; // 3D坐标 -} SVzNLColorMark3DResult; - -/// @brief ROI -typedef struct -{ - int nPointIdx; // 点序 - SVzNL2DPoint ptLeft2D; // 2D左图坐标 - SVzNL2DPoint ptRight2D; // 2D右图坐标 - SVzNLRect sLeftBox; - SVzNLRect sRightBox; -} SVzNLColorMark2DResult; - -/// @brief ROI -typedef struct -{ - int nPointIdx; // 点序 - EVzNLColorMarkType eType; - SVzNL3DPoint pt3D; - SVzNL2DPoint ptLeft2D; // 2D左图坐标 - SVzNL2DPoint ptRight2D; // 2D右图坐标 - SVzNLRect sLeftBox; - SVzNLRect sRightBox; -} SVzNLColorMarkResult; - -typedef struct -{ - SVzNL3DPosition* p3DPoint; - int nCount; -}SVzDetectGrainsLaserResult; - -typedef struct -{ - unsigned int nIndex; - float fLength; - float fWidth; - float fHeight; - - float fVolume; //until mm - SVzNL3DPoint sObjCenter; - SVzNL3DPoint sBoxGoundTop[4]; - SVzNL3DPoint sBoxGoundBottom[4]; -} SVzLaserGrainsData; - -typedef struct -{ - SVzLaserGrainsData* pGrainData; - int nCount; -} SVzDetectGrainResult; - -typedef enum -{ - keGrainLaserStatus_Unknown = 0, - keGrainLaserStatus_BeginWork, - keGrainLaserStatus_Working, - keGrainLaserStatus_EndWork, -} EVzGrainLaserStatus; - -///< 每个石子的信息 -typedef struct -{ - int index_; - char lithology[128]; //岩性 - char origin_[128]; //产地 - double length_; //长 - double width_; //宽 - double height_; //高 - double vol_; //体积 - double area_; //面积 - double perimeter_; //周长 - float A_; //Axial factor 轴向系数 - float CR_; //Convexity 凸度 - float AP_; //棱角参数 - VzBool isNeedle_; //是否针片状 - SVzNLRect rcPos; -} SVzLaserAggregateData; - -///< 级配信息 -typedef struct -{ - int index_; // 级配级别 6 7 8 9 10 11 12 13 - int num_; // 数量 - float pi_; // pi - float passRate_; // 通过率 - float top_; // 上限 - float bottom_; // 下限 -} SVzLaserAggregateLevelData; - -///< 石子检测综合信息 -typedef struct -{ - char chLithology[128]; ///< 岩性 - char chOrigin[128]; ///< 产地 - float fNeedleRate; ///< 针片状含量 - float fMeanAP; ///< 综合棱角值 - float fMeanA; ///< 综合轴向系数 - float fMeanCR; ///< 综合凸度 -} SVzLaserAggregateMeanInfo; - -///< 石子检测状态 -typedef enum -{ - keAggerateLaserStatus_Unknown = 0, - keAggerateLaserStatus_BeginWork, ///< 开始工作 - keAggerateLaserStatus_Working, ///< 正在工作 - keAggerateLaserStatus_EndWork, ///< 结束工作 -} EVzAggerateLaserStatus; - -/// @brief -/// 钢筋信息 -typedef struct -{ - int nFlag; - int nRadius; - float fPhyRadius; - SVzNL2DPoint nPoint; - int nLineIndex; - unsigned int nTimeStamp; - SVzNL3DPoint n3DPoint; -}SVzNLCounterItem; - -/// @brief -/// 钢筋信息 -typedef struct -{ - int nNum; // 总数 - SVzNLCounterItem* pPointArr; // 二维扫描图坐标 - unsigned long long nStartTimeStamp;// 起始线的时间戳 - SVzNLPosition sTopPoint; // 最突出点数据 - SVzNLCounterItem sMarkPoint; // mark信息 - SVzNLImageData sImage; // 生成的二维图像 -} SVzNLCounter; - -/// @brief -typedef struct -{ - unsigned int nSize; - char szName[64]; - char szVersion[64]; - void* pVoid; - int nVersionCode; -} SVzAICreateParam; - -/** - * @brief - * 输出帧参数 - */ -typedef struct -{ - unsigned long long nFrameIdx; //< 帧号 - unsigned long long nTimeStamp; //< 时间戳 -} SVzOutputFrameProps; - -/** - * @brief - * Circle Mark Information - */ -typedef struct -{ - int nMarkID; //< Mark 标志 - SVzNL3DPosition ptCenterPos; //< 中心点 - SVzNL3DPosition ptLeftPos; //< 左 - SVzNL3DPosition ptTopPos; //< 上 - SVzNL3DPosition ptRightPos; //< 右 - SVzNL3DPosition ptBottomPos; //< 下 - unsigned int nGray; -} SVzCircleMarkPosInfo; - -/// @brief -/// 3D圆形 -typedef struct -{ - float fR; - SVzNL3DPointF ptCenter; - SVzNL3DPointF ptWordCenter; - SVzNL3DPointF ptNormalVector; - SVzNL3DPointF* pArray3DPoint; - SVzNL3DPointF* pArrayWorld3DPoint; - int nPointCount; -} SVzNL3DCircleResult; - -/// @brief -/// 3D圆形物体 -typedef struct -{ - EVzNL3DCircleType eType; // 圆类型 - - SVzNLROIRect box2dLeft; // 包围盒左图 - SVzNLROIRect box2dRight; // 包围盒右图 - - SVzNL3DCircleResult* pArrayCircle; - int nCircleCount; -} SVzNL3DCircleObjectResult; - -/// @brief -/// 3D圆形 -typedef struct -{ - double dR; - SVzNL2DPoint ptLeftCenter; - SVzNL2DPoint ptRightCenter; - SVzNL2DPoint* pArray2DLeftPoint; - SVzNL2DPoint* pArray2DRightPoint; - int nPointCount; -} SVzNL2DCircleResult; - -/// @brief -/// 3D圆形物体 -typedef struct -{ - EVzNL3DCircleType eType; // 圆类型 - - SVzNLROIRect box2dLeft; // 包围盒左图 - SVzNLROIRect box2dRight; // 包围盒右图 - - SVzNL2DCircleResult* pArrayCircle; - int nCircleCount; -} SVzNL2DCircleObjectResult; - -/* - * @brief 眼睛配置信息 - */ -typedef struct -{ - EVzNLEthernetEyeIPType eEthernetIPType; - unsigned char byEyeIP[4]; - unsigned char byEyeMac[6]; - unsigned char byLocalIP[4]; - unsigned char byLocalMac[6]; - char vendor[8]; - char productName[8]; - char serialID[8]; - VzBool bIsSupportUDPFilter; - - SVzNetCardInfo sNetCardInfo; -} SVzNLEthernetEyeConfigInfo; - -typedef struct -{ - double dWhiteCheckPercent; - unsigned char chRgbMax[3]; - double dBlockEffectiveTh; - short shMinBlockNumber; - double dGainRange[2]; - unsigned char chHistStartAddr; - int nBlockWidth; - int nBlockHeight; -} SVzNLAWBInfo; - -// 钢筋检测模式 -enum -{ - keEyeCBDetectCounterMode_All = 1, - keEyeCBDetectCounterMode_Center = 2, -}; -typedef unsigned int EVzEyeCBDetectMode; - -// @brief 摆动机构信息 -struct SVzSwingMotorDevInfo -{ - int nMotorMaxAngle; //< 电机最大旋转角度 - int nMaxAngle; //< 最大旋转角度 - int nMinSpeed; //< 最小角速度 - int nMaxSpeed; //< 最大角速度 - int nStartAngle; //< 起始位置 - int nEndAngle; //< 结束位置 - int nSpeed; //< 速度 - int nStopAngle; //< 停止角度 - float fEndAngleThres; //< 结束角度判断阈值 -}; - -/// @brief -typedef struct -{ - unsigned int nAirforceCount; //< 气枪数 - unsigned int nStartAddress; //< 起始位置 - double dVolumeThres; //< 体积阈值 - unsigned int nMaxAirTimes; //< 最大喷气次数 - unsigned int nTimePerAir; //< 单次喷气时间 ms -} SVzDetectGangueParam; - -/// @brief -typedef struct -{ - double dArea; - SVzNL3DPoint ptPos[8]; -} SVzGangueObjectResult; - -/// @brief -/// Dump文件回调 -/// @param [out] szCrashFile Dump全路径 -typedef void(*VzNL_CrashCallBack)(char szCrashFile[VZ_MAXPATH]); - -/// @name 自动检测回调函数 -/// @{ -/** - * @brief 获取自动检测结果 - * @param [out] p3DPoint 3D数据 - * @param [out] p2DPoint 2D数据 - * @param [out] nCount 数据个数 - * @param [out] nTimeStamp 时间戳 - * @param [out] dTotleOffset 总偏移 - * @param [out] dStep 单次偏移 - * @param [out] pLeftImage 左图 - * @param [out] pRightImage 右图 - * @param [out] pParam 回调参数 - */ -typedef int(*VzNL_GetAutoDetectResultCB)(SVzNL3DPosition* p3DPoint, SVzNL2DPosition* p2DPoint, int nCount, unsigned long long nTimeStamp, double dTotleOffset, double dStep, SVzNLImageData* pLeftImage, SVzNLImageData* pRightImage, void* pParam); - -/// @brief 激光线数据 -typedef struct -{ - void* p3DPoint; //< 3D点 根据ResultDataType使用不同的数据结构 - void* p2DPoint; //< 2D点 根据ResultDataType使用不同的数据结构 - int nPointCount; //< 2D 3D 数据个数 - double dTotleOffset; //< 总偏移 - double dStep; //< 距离上一条线的偏移 - unsigned long long llFrameIdx; //< 帧号 - unsigned long long llTimeStamp; //< 时间戳 - unsigned int nEncodeNo; //< 编码器值 - float fSwingAngle; //< 当前摆动角度 - VzBool bEndOnceScan; //< 是否已完成一次扫描 -} SVzLaserLineData; - -typedef void(*VzNL_AutoOutputLaserLineExCB)(EVzResultDataType eDataType, SVzLaserLineData* pLaserLinePoint, void* pParam); - -typedef struct -{ - unsigned int nCount; - EVzResultDataType eDataType; - SVzLaserLineData* pLaserLineData; -}SVzLaserLineCacheData; - -/// @brief 激光线数据 -typedef struct -{ - SVzLaserLineData sLineData; //< - - SVzNLImageData* pImageData[3]; //< 图像数据 - void* pUnknownData; //< 未知数据 -} SVzLaserLineDataEx; - -/// @} - -/// @brief 检测码类型 -enum -{ - keLogisticsDetectCodeTypeNone = 0, //< 什么都不检测 - keLogisticsDetectCodeType_QRCode = 0x01, //< 二维码 - keLogisticsDetectCodeType_BarCode = 0x02, //< 条形码 -}; -typedef unsigned int EVzLogisticsDetectCodeType; - -/// @brief 扣图类型 -enum -{ - keLogisticsClipObjModeNone = 0, //< 不做抠图操作 - keLogisticsClipObjMode_Object = 0x01, //< 抠物体图 - keLogisticsClipObjMode_QRCode = 0x02, //< 抠二维码图 - keLogisticsClipObjMode_BarCode = 0x04, //< 抠条形码图 -}; -typedef unsigned int EVzLogisticsClipObjMode; - -// 物流触发位置 -enum -{ - keLogisticsPackageSyncTypeInvalid = 0, //< 不触发 - keLogisticsPackageSyncType_In, //< 物体进入时触发 - keLogisticsPackageSyncType_Out, //< 物体出来时触发 -}; -typedef unsigned int EVzLogisticsPackageSyncType; - -// 物流相机运行模式 -typedef enum -{ - keLogisticsRunMode_None = 0, //< 无 - keLogisticsRunMode_SoftSignal = 1, //< 软件使能开始检测 - keLogisticsRunMode_ExtSignal, //< 外部使能开始检测 - keLogisticsRunMode_AutoSignal, //< 自动检测 -} EVzLogisticsRunMode; - -// 物流结果状态 -typedef enum -{ - keLogisticsResultStatusInvalid = 0, //< 无效 - keLogisticsResultStatus_Begin, //< 物体开始 - keLogisticsResultStatus_Work, //< 物体运动中 返回点云数据 - keLogisticsResultStatus_End //< 当前物体结束 返回物体体积 -} EVzLogisticsResultStatus; - -// @brief 结果信息掩码 -enum -{ - keLogisticsResultMask_PointCloud = 0x01, //< 点云数据 - keLogisticsResultMask_ObjectInfo = 0x02, //< 物体结果数据 - keLogisticsResultMask_ColorImage = 0x04, //< 彩色图像 -}; -typedef unsigned int EVzLogisticsResultMask; - -// @brief 硬件使能线信号类型 -enum -{ - keEnLineInPoint_None = 0x00, //< 无效 - keEnLineInPoint_EnSignal = 0x01, //< 使能信号 - keEnLineInPoint_BiPhaseEncoder = 0x02, //< 双向编码器 -}; -typedef unsigned int EVzEnLineInPoint; - -// @brief -typedef struct -{ - char szCode[VZ_MAXPATH]; //< 码值 - VzBool bValidImage; - SVzNLImageData sCodeImageData; //< 码图像 - void* pVoid; -} SVzLogisticsCodeInfo; - -// @brief 物体结果描述 -typedef struct -{ - float fVolume; //< 体积 - float fLength; //< 长 - float fWidth; //< 宽 - float fHeight; //< 高 - - // ①------------② - // /| /| - //③------------④ | - //| | | | - //| | | | - //| | | | - //|⑤------------⑥ - // / / - //⑦------------⑧ - SVzNL3DPoint sConnerPoint[8]; //< 8角点 - SVzNL3DPoint* pOutlinePoint; //< 轮廓点 - unsigned int nOutlinePointCount; //< 轮廓点个数 - - SVzLogisticsCodeInfo* psQRCodeInfo; //< 二维码信息 - unsigned int nQRCodeCount; - SVzLogisticsCodeInfo* psBarCodeInfo; //< 条形码信息 - unsigned int nBarCodeCount; - - VzBool bValidObjImage; //< 物体图像有效 - SVzNLImageData sObjImageData; //< 物体图像 - void* pVoidObjData; -} SVzLogisticsObjInfo; - -// 物流结果信息 -typedef struct -{ - EVzLogisticsResultMask eMask; //< 结果标志 - - // @name PointClound - // @{ - unsigned long long ullTimeStamp; //< 时间戳 - unsigned long long ullEncodeNo; //< 编码器值 - SVzNL3DPosition* p3DPoint; //< 3D点 - unsigned int nPointCount; //< 点数 - double dOffset; //< 偏移 - // @} - - // @name ObjectInfo - // @{ - unsigned int nObjectCount; //< 物体个数 - SVzLogisticsObjInfo* pArrayObjectInfo; //< 物体数组 - - VzBool bIsResetOffset; //< 当前帧开始X值被重置 - // @} - - // @name ColorImage - // @{ - SVzNLImageData* psCenterImage; //< 图像 - unsigned int nCenterImageTimeStamp; //< 图像时间 - // @} -} SVzLogisticsResultData; - -typedef enum -{ - keLaserFileType_Invalid = 0, - keLaserFileType_PureTxt = 1, //< 纯文本 - keLaserFileType_Txt, //< 本公司自用 - keLaserFileType_Pcd, //< PCD - keLaserFileType_Las, //< LAS - keLaserFileType_VzBinaryData, //< Binary File - keLaserFileType_Ply, -} EVzLaserFileType; - -/// @brief 文件模式 -enum -{ - keFileModeInvalid = 0, - keFileModeRead = 0x01, - keFileModeWrite = 0x02, -}; -typedef unsigned int EVzFileMode; - -/// @brief 点云后处理图像类型定义 -typedef enum -{ - kePointAfterProcTypeInvalid = -1, - kePointAfterProcType_DepthMap, // 深度图 - kePointAfterProcType_GrayPic, // 灰度图 - kePointAfterProcType_Intensity, // 光强图 -} EVzPointAfterProcType; - -/* - * 机械臂点数据结构定义 - */ -typedef struct -{ - float x; - float y; - float z; - float roll; - float pitch; - float yaw; -} SVzNLRobotPointF; - -typedef struct -{ - SVzNLRobotPointF sPoint; // 坐标系内的点坐标 - SVzNL3DPointF sRX; // 坐标系 X方向 - SVzNL3DPointF sRY; // 坐标系 Y方向 - SVzNL3DPointF sRZ; // 坐标系 Z方向 -} SVzNLRobotPointWithPoseF; - -typedef struct -{ - SVzNL3DPointF sPoint; // 坐标系内的点坐标 - SVzNL3DPointF sRX; // 坐标系 X方向 - SVzNL3DPointF sRY; // 坐标系 Y方向 - SVzNL3DPointF sRZ; // 坐标系 Z方向 -} SVzNL3DPosePointF; - -// -typedef struct -{ - SVzNL3DPointF sPoint; // 点位置 - SVzNL3DPointF sNormal; // 法向量 -} SVzNL3DTracePointF; - -/// @brief 激光线数据 -typedef struct -{ - SVzLaserLineData sLineData; - - unsigned int nLeftPointCount; - unsigned int nRightPointCount; - - SVzNLPointXYZ* pLeftTearData; - SVzNLPointXYZ* pRightTearData; - unsigned int nLeftTearPointCount; - unsigned int nRightTearPointCount; -} SVzLaserTearDataEx; - -/// @brief -typedef enum -{ - keLaserObjectDetectPresetProfile_Custom = -1, - keLaserObjectDetectPresetProfile_General, //< 通用 - keLaserObjectDetectPresetProfile_ThinWire, //< 细铁丝 - keLaserObjectDetectPresetProfile_BigSence, //< 大景深 - keLaserObjectDetectPresetProfile_ReBar, //< 钢筋计数 - keLaserObjectDetectPresetProfileLast -} EVzLaserObjectDetectPresetProfile; - - -/// @brief -/// 激光线文件配置参数 -typedef enum -{ - keLaserFileParamStart = 0, - keLaserFileParam_Speed = keLaserFileParamStart, //< 速度值 类型 double 当前用于 binarydata 和 txt 两种文件存储 - keLaserFileParam_DataType, //< 数据类型 类型 unsigned int 当值为 0时表示Ascii模式,当为1时表示Binary模式,目前用于pcd的配置 - keLaserFileParam_SavePointCnt, //< 是否存储point个数 类型 unsigned int ,当值为0时表示不存储,为1表示存储,目前用于pure txt的配置 - keLaserFileParam_SaveSwingAngle, - keLaserFileParamLast -} EVzLaserFileParam; - -/// @brief -/// 触发输出模式 -typedef enum -{ - keStrobeTriggerOutMode_None = 0, //< 不控制触发输出 : 默认 - keStrobeTriggerOutMode_CameraLink, //< 跟随主模式输出 - keStrobeTriggerOutMode_UserControl, //< 用户控制 - keStrobeTriggerOutMode_LoopBack, //< 回环控制 - keStrobeTriggerOutMode_ServoControl,//< 舵机控制 -} EVzStrobeTriggerOutMode; - -/// @brief -/// Sensor 是否为长焦镜头 -enum EVzSensorLenType -{ - keSensorLenType_Invalid = 0, - keSensorLenType_3mm, - keSensorLenType_6mm, -}; - -/// @brief -/// 镜头盖住的状态类型 -enum EVzServoStatusType -{ - keServoStatusType_Invalid = -1, - keServoStatusType_Cover, - keServoStatusType_UnCover, -}; - -/// @brief -/// 编码器参数类型 -enum EVzEncodeParamType -{ - keEncodeParamType_RollerResolution = 0, //< 使用 编码器分辨率 + 编码器半径参数 模式 - keEncodeParamType_DistancePerPulse, //< 使用 编码器每两个脉冲的间距 -}; - -/// @brief -/// 配置多相机触发模式 -enum EVzMultiDevWorkMode -{ - keMultiDevWorkMode_MasterSlave = 0, ///< 主从触发 - keMultiDevWorkMode_Ext_FallingEdge = 1, ///< 外部下降沿触发 - keMultiDevWorkMode_Ext_HighFrequenceFallingEdge = 2, ///< 外部高帧率下降沿触发 -}; - -/// @brief -/// 配置融合的相机类型 -enum EVzMultiDevType -{ - keMultiDev_Dynanmic = 0, ///< 动态相机融合 - keMultiDev_Static = 1, ///< 静态相机融合 -}; - -/// @brief -/// 配置静态相机融合扫描模式 -enum EVzMultiStaticDevScanMode -{ - keMultiStaticDevScanMode_OneByOne = 0, ///< 一个接一个的扫描 - keMultiStaticDevScanMode_SameTime = 1, ///< 同时扫描 -}; - -/// @brief -/// 编码器方向 -enum EVzEncoderRollingDirection -{ - keEncoderRollingDirection_Forward = 0, - keEncoderRollingDirection_Backward -}; - - -/// @brief -/// 旋转图像类型 -enum EVzImageRotateType -{ - keImageRotateType_Invalid = -1, - keImageRotateType_FlipY_Rotate = 0, - keImageRotateType_ClockWise = 1, - keImageRotateType_CounterClockWise = 2, - keImageRotateType_90ClockWise = 3, - keImageRotateType_FlipX = 4, -}; - -typedef struct -{ - unsigned int nEncodeNo[2]; - EVzEncoderRollingDirection eDirection; - unsigned int nFrequency[2]; -} SVzEncoderData; - -enum -{ - keEmulationDetectType_AutoDetect = 0, - keEmulationDetectType_Detect, -}; -using EVzEmulationDetectType = unsigned int; - -/// @brief -/// 旋转图像类型 -enum EVzDustCoverConfigType -{ - keDustCoverConfigType_Large = 0, - keDustCoverConfigType_Medium, - keDustCoverConfigType_Small, -}; - -/// @brief -//环境类型 -typedef enum -{ - keEnvType_Invalid = -1, - keEnvType_InDoor = 0, - keEnvType_OutDoor = 1, -} EVzEnvType; - -/// @brief 范围类型 int -typedef struct -{ - EVzEnvType eEnvType; - unsigned int nLevel; -} SVzEnvLevel; - -/// @brief -//抗反光增益 -typedef enum -{ - keAntiReflectGainType_Close = 1, - keAntiReflectGainType_Double = 2, - keAntiReflectGainType_Quadruple = 4, - keAntiReflectGainType_Sextuple = 6, - keAntiReflectGainType_Octuple = 8, -} EVzAntiReflectGainType; - -typedef enum -{ - keAntiReflectCmpsDirType_LeftToRight = 0, - keAntiReflectCmpsDirType_RightToLeft = 1, -} EVzAntiReflectCmpsDirType; - -typedef struct -{ - unsigned int m_nCheckTimeInterval; - unsigned int m_nCheckTimeOut; - unsigned int m_nCheckExposeRatio; -}SVzAutoCoverControlParam; - -typedef struct -{ - char chIP[VZNL_SDK_NETWORK_IPv4_LENGTH]; -}SVzNLDeviceIp; - -typedef enum -{ - keLRSensorType_Gray = 0, - keLRSensorType_Color = 1, -} EVzLRSensorType; - -typedef enum -{ - keRGBOutputChannelType_R = 0, - keRGBOutputChannelType_G = 1, - keRGBOutputChannelType_B = 2, - keRGBOutputChannelType_All = 3, -} EVzRGBOutputChannelType; - -/// @brief -// 多相机融合时各子设备融合配置参数 -typedef struct -{ - unsigned int nDevIdx; // 融合索引:0:主设备;>0:从设备; - double dFusionMatrix[3][4]; // 融合矩阵 - double dYRange[2]; // 当前设备融合时Y轴过滤范围 -}SVzNLDeviceFusionData; - -// RGBHDR 出图模式 -typedef enum -{ - keRGBHDROutImageMode_Ori = 0, // 输出原始图像:背景图 + 熔池图 - keRGBHDROutImageMode_HDR = 1, // 输出HDR 叠加后的图像:HDR图 - keRGBHDROutImageMode_MPool = 2, // 只输出熔池图像 - keRGBHDROutImageMode_BG = 3, // 只输出背景图 - keRGBHDROutImageMode_Num // 模式个数 -} EVzRGBHDROutImageMode; - -/** -* @brief 点云<->2D伪彩图转换参数结构体 -*/ -typedef struct -{ - unsigned int nEdgeMargin; - double d3DXMin; - double d3DXMax; - double d3DYMin; - double d3DYMax; - double dPixelResolution; -}SVzPColorImageConvertInfo; - -/** -* @brief 2D伪彩图坐标<->3D坐标选取方法枚举 -*/ -typedef enum -{ - kePColorImageTo3DPoint_NearestPoint = 0, // 输出距离回算点X/Y 最近的原始点云3D 点坐标。 - kePColorImageTo3DPoint_RealPoint // 输出实际回算点X/Y坐标 + 最邻近原始点的Z坐标。 -}SVzPColorImageTo3DPointMethod; - -/// @brief -/// 采集图像模式 -typedef enum -{ - keCaptureMode_LR_Image = 0, // 输出双目图像 - keCaptureMode_C_Image, // 输出RGB图像 - keCaptureMode_LRC_Image, // 输出双目+RGB图像 -}EVzCaptureMode; - -/// @brief 焊缝跟踪点云扫描配置参数(存储到相机内) -typedef struct -{ - // 可配参数部分 - bool bIsEnableCalibROI; // 是否启用校正图 - EVzNLExposeMode eExposeMode; // 曝光模式 - unsigned int nExposeTime; // 曝光值 - unsigned short nLeftCameraGain; // 左目增益 - unsigned short nRightCameraGain; // 右目增益 - unsigned short nDynamicScanAngle; // 动态扫描时摆动机构角度 - unsigned short nFltrRgnTh; // 小信号过滤值 - SVzNLROIRect sLeftROI; // 左目ROI - SVzNLROIRect sRightROI; // 右目ROI - unsigned int nFrameRate; // 采集帧率 - unsigned int nOutputFrameRate; // 输出帧率:对接收到的激光线进一步抽样输出(0:不进行抽帧处理) - // 固定参数部分(SDK 内部自动设置),无需修改 - bool bIsSwingEnable; // 是否使能摆动机构,固定为false - bool bIsRGBEnable; // 是否使能RGB,固定为false - bool bIsStrobeEnable; // 是否使能C版功能,固定为true - bool bIsAntiReflectGainEnable; // 是否使能抗强光增益,固定为true -} SVzTrackingScanPara; - - -typedef struct{ - int denoiseStrength; // 去噪强度 - int sharpenStrength; // 锐化强度 - int brightness; // 亮度 - int contrast; // 对比度 - bool useAdvancedDenoise; // 使用高级去噪 -}AlgorithmParams; - -/** -* @brief 多相机融合操作,重叠区数据处理方法 -*/ -typedef enum -{ - keMulDevOverlayProcessType_SeqFilter = 0, // 按Y轴顺序过滤重叠的点云 - keMulDevOverlayProcessType_PointsFilter, // 按重叠区域内点数据个数过滤数据少的相机的点云 - keMulDevOverlayProcessType_DataFusion, // 对重叠区域内的点数据进行数据融合,补齐缺失的数据 -}EVzMulDevOverlayProcessType; - - -// Feature标志位定义 -typedef enum -{ - KE_FEATURE_RGB_CAMERA = 0x01, // RGB相机 - KE_FEATURE_SWING_MECHANISM = 0x02, // 摆动机构 - KE_FEATURE_DUAL_OUTPUT = 0x04, // 双端出图 - KE_FEATURE_STEREO_COLOR = 0x08, // 双目彩色 - KE_FEATURE_WELDING_CAMERA = 0x10 // 焊接相机 -}EVzFeatureFlags; - -// Feature数据结构 -typedef struct -{ - unsigned short flags; // 特性标志位 - unsigned short pupilDistance; // 相机瞳距配置 -}SVzFeatureData; - -/* -* 命令操作类型定义 -*/ -enum EVzUSBSerialRelayCmdType -{ - keUSBSerialRelayCmdType_CloseNoAck = 0, // 关闭不反馈 - keUSBSerialRelayCmdType_OpenNoAck, // 打开不反馈 - keUSBSerialRelayCmdType_CloseWithAck, // 关闭并反馈 - keUSBSerialRelayCmdType_OpenWithACck, // 打开并反馈 - keUSBSerialRelayCmdType_ReverseWithAck, // 取反并反馈 - keUSBSerialRelayCmdType_CheckState, // 查询状态 - keUSBSerialRelayCmdType_FlashBreakWithAck, // 闪断并反馈 - keUSBSerialRelayCmdType_Num -}; - -/* -* 串口波特率枚举 -*/ -enum EVzSerialPortBaudRate -{ - keSerialPortBaudRate_1200 = 0, - keSerialPortBaudRate_2400, - keSerialPortBaudRate_4800, - keSerialPortBaudRate_9600, - keSerialPortBaudRate_19200, - keSerialPortBaudRate_38400, - keSerialPortBaudRate_57600, - keSerialPortBaudRate_115200, - keSerialPortBaudRate_Num -}; - -/* -* 串口数据长度枚举(4~8bits) -*/ -enum EVzSerialPortByteSize -{ - keSerialPortByteSize_4 = 0, - keSerialPortByteSize_5, - keSerialPortByteSize_6, - keSerialPortByteSize_7, - keSerialPortByteSize_8, - keSerialPortByteSize_Num -}; - -/* -* 串口数据校验位枚举(0-4=None,Odd,Even,Mark,Space) -*/ -enum EVzSerialPortParity -{ - keSerialPortParity_None = 0, - keSerialPortParity_Odd, - keSerialPortParity_Even, - keSerialPortParity_Mark, - keSerialPortParity_Space, - keSerialPortParity_Num -}; - -/* -* 串口数据停止位枚举(0,1,2 = 1, 1.5, 2) -*/ -enum EVzSerialPortStopBits -{ - keSerialPortStopBits_1 = 0, - keSerialPortStopBits_1_5, - keSerialPortStopBits_2, - keSerialPortStopBits_Num -}; - -/* -* 串口配置参数结构 -*/ -typedef struct -{ - EVzSerialPortBaudRate eBaudRate; // 串口波特率 - EVzSerialPortByteSize eByteSize; // 串口波特率 - EVzSerialPortParity eParity; // 串口校验位 - EVzSerialPortStopBits eStopBits; // 串口停止位 -}SVzSerialPortCfgPara; - -/* -* USB 继电器设备参数 -*/ -typedef struct -{ - unsigned int nChannelNum; // 模块控制的通道个数 - SVzSerialPortCfgPara sSerialPortCfgPara; // 串口控制参数 -}SVzUSBSerialRelayCfgPara; - -/* -* 搜索输出的信息结构,目前只包括设备端口名称 -*/ -typedef struct -{ - char sPortName[VZNL_DEVICE_NAME_LENGTH]; -}SVzSerialPortInfo; - -/* -* -*/ -typedef struct -{ - char szName[VZ_USERCONFIG_NAME_LENGTH]; - char szDesc[VZ_USERCONFIG_DESC_LENGTH]; -}SVzUserConfigInfo; - - -#endif //__VIZUM_TYPES_HEADER_ +/* + * Header: vizum_type.h + * Description:当前文件为双目相机(Vizum)研发的EyeCB板提供所有类型的定义。 + * Author: Mjw + * Date: 2018/08/28 + * Update: 2023/04/27 + */ + +#ifndef __VIZUM_TYPES_HEADER__ +#define __VIZUM_TYPES_HEADER__ + +/// @name 网络相关定义 +/// @{ +/// @brief +/// 网络IP长度 +#define VZNL_SDK_NETWORK_IPv4_LENGTH 16 + +/// @brief +/// 网络MAC长度 +#define VZNL_SDK_NETWORK_MAC_LENGTH 18 +/// @} + +/// @breif +/// 版本字符串长度 +#define VZNL_VERSION_LENGTH 64 + +/// @brief +/// 设备名称长度。 +#define VZNL_DEVICE_NAME_LENGTH 64 + +/// @brief +/// 唯一标识符大小 +#define VZNL_GUID_LENGTH 64 + +/// @brief +/// 设备名称长度 +#define VZ_DEVICE_NAME 64 + +/// @brief +/// 设备描述长度 +#define VZ_DEVICE_DESC 256 + +/// @brief +/// 设备ID长度 +#define VZNL_DEVICEID_LENGTH 64 + +/// @brief +/// 设备地址 +#define VZNL_DEVICE_ADDR_LENGTH 64 + +/// @brief +/// File Path Length +#define VZ_MAXPATH 256 + +/// @brief UserProfile MAX Count +#define VZ_USERPROFILE_MAX_COUNT 10 + +#define VZ_USERCONFIG_MAX_COUNT 200 + +/// @brief UserProfile Name Length +#define VZ_USERPROFILE_NAME_LENGTH 64 + +#define VZ_USERCONFIG_NAME_LENGTH 64 + +#define VZ_USERCONFIG_DESC_LENGTH 256 + + +/// @brief +/// 设备句柄 +typedef void* VZNLHANDLE; + +/// @brief +/// 设备句柄 +typedef void* VZNLFILE; + +/// @brief +/// 检测工具句柄 SDK 2.2.0开始支持 +typedef void* VZNLDETECTHANDLE; + +/// @brief +/// 数据处理句柄 +typedef void* VZNLPOINTCLOUDHANDLE; + +/// @brief +/// 数据处理句柄 +typedef void* VZNLIMAGEGENERATOR; + +/// @brief +/// RGB点云处理工具句柄 +typedef void* VZNLRGBCLOUDPOINTTOOL; + +enum +{ + VzFalse = 0, + VzTrue = 1, +}; +typedef unsigned char VzBool; + +/// @brief +/// 颜色定义 +typedef unsigned int VzColorRGB; //RGB + +/// @brief +/// Data Type +#ifdef _WIN64 + typedef unsigned long long VzParam; +#else + typedef unsigned long VzParam; +#endif + +typedef struct +{ + unsigned short shYear; + unsigned short shMonth; + unsigned short shDay; + unsigned short shHour; + unsigned short shMinute; + unsigned short shSecond; + unsigned short shWeek; +} SVzDateTime; + +// 设备搜索标志 +enum +{ + keSearchDeviceFlagNone = 0x0000, //< 不搜索 + keSearchDeviceFlag_EyeCB = 0x0001, //< 搜索控制器 + keSearchDeviceFlag_USBLargeEye = 0x0002, //< 搜索USB极光眼 + keSearchDeviceFlag_EthLargeEye = 0x0004, //< 搜索网络星光眼:激光 + keSearchDeviceFlag_EthSmallEye = 0x0008, //< 搜索网络星光眼:智能 + keSearchDeviceFlag_EthLaserRobotEye = 0x0010, //< 搜索网络智光眼:激光 + keSearchDeviceFlagAll = keSearchDeviceFlag_EthLaserRobotEye, //< 搜索全部支持的设备 +}; +typedef unsigned int EVzSearchDeviceFlag; + +/// @brief +/// 眼睛分辨率枚举值 +typedef enum +{ + keVzNLEyeResolution_Unknown = 0, + keVzNLEyeResolution_130WH, ///< 130W像素的眼睛 1280 * 960 + keVzNLEyeResolution_130WV, ///< 130W像素的眼睛 960 * 1280 + keVzNLEyeResolution_300WH, ///< 300W像素的眼睛 2048 * 1520 + keVzNLEyeResolution_300WV, ///< 300W像素的眼睛 1536 * 2048 + keVzNLEyeResolution_1300W, ///< 1300W像素的眼睛 + keVzNLEyeResolutionLast +} EVzNLEyeResolution; + +/// @brief +/// 设备类型 +typedef enum +{ + keDeviceType_Unknown = 0, + keDeviceType_EyeCB, // EyeCB控制器 + keDeviceType_LaserEye, // 星光眼:激光 + keDeviceType_SmartEye, // 星光眼:智能 + keDeviceType_LaserRobotEye, // 智光眼:激光 + keDeviceType_SmartRobotEye, // 智光眼:智能 +} EVzDeviceType; + +/// @brief +/// 眼睛类型标志 +enum +{ + keEyeTypeFlag_Unknown = 0x00, + keEyeTypeFlag_USB = 0x01, //< USB + keEyeTypeFlag_Eth = 0x02, //< 网络 + + keEyeTypeFlag_Color = 0x10, //< 彩色 + keEyeTypeFlag_Gray = 0x20, //< 黑白 + + keEyeTypeFlag_Laser = 0x100, //< 极光眼 + keEyeTypeFlag_Smart = 0x200, //< 智能眼 + + keEyeTypeFlag_Product1 = 0x1000, //< 小眼睛 + keEyeTypeFlag_Product2 = 0x2000, //< 大眼睛 + keEyeTypeFlag_Product3 = 0x3000, //< 大眼睛 +}; +typedef unsigned int EVzEyeTypeFlag; + +enum +{ + keEyeSensorType_Left = 0x01, + keEyeSensorType_Right = 0x02, +}; +typedef unsigned int EVzEyeSensorType; + +/// @bried +/// 眼睛出图方式 +typedef enum +{ + keEyeImgMode_Low = 1, + keEyeImgMode_Middle, + keEyeImgMode_High, + keEyeImgMode_Higher, + keEyeImgMode_Hights, + keEyeImgMode_Algo +}EVzEyeImgBitMode; + +/// @brief +/// 运动方向 +typedef enum +{ + keObjRunDirect_Plus = 0, //< 正方向 + keObjRunDirect_Minus = 1, //< 反方向 +} EVzObjRunDirect; + +/// @brief +/// 电动机运行方向 +typedef enum +{ + keEncodeMotorRunDirectNone = 0, // 未知 + keEncodeMotorRunDirect_ClockWise, // 顺时针 + keEncodeMotorRunDirect_CounterClockWise, // 逆时针 +} EVzEncodeMotorRunDirect; + +/// @brief +/// 外部触发模式 +enum +{ + keEyeTriggerMode_Invalid = -1, // 无效值 + keEyeTriggerMode_Slave = 0, // 被动触发 + keEyeTriggerMode_Master = 1, // 主动模式 + keEyeTriggerMode_FallingEdge = 2, // 下降沿触发 + keEyeTriggerMode_RisingEdge = 3, // 上升沿触发 + keEyeTriggerMode_LowLevelEnable = 4, // 低电平使能数据输出 + keEyeTriggerMode_ManualTrigger = 6, // 手动模式 + keEyeTriggerMode_Package = 9, // 物流包裹触发模式 + keEyeTriggerMode_HighFrequenceFallingEdge = 10, // 高帧率下降沿触发 +}; +typedef unsigned int EVzEyeTriggerMode; + +/// @brief +/// 设置点云计算模式 +typedef enum +{ + kePointCloudProcMode_Invalid = 0, //< 无效 + kePointCloudProcMode_Speed, //< 速度偏移模式 + kePointCloudProcMode_Encoder, //< 编码器模式 + kePointCloudProcMode_FixedStep, //< 固定间隔模式 +} EVzPointCloudProcMode; + +/// @brief +/// 设备状态 +typedef enum +{ + keDeviceWorkStatusUnknown = 0, //< 未知 + keDeviceWorkStatus_Eye_Comming = 1, //< 眼睛连接 + keDeviceWorkStatus_Eye_Reconnect, //< 眼睛重连 + keDeviceWorkStatus_Working, //< 工作中 + keDeviceWorkStatus_Offline, //< 离线 + keDeviceWorkStatus_Eye_TimeOut, //< 眼睛超时 + keDeviceWorkStatus_Eye_UnStability, //< 眼睛不稳定 + keDeviceWorkStatus_EyeCB_Mem, //< 内存 + keDeviceWorkStatus_EyeCB_TimeStampDiffErr, //< 时间戳错误 + keDeviceWorkStatus_Device_Swing_Finish, //< 摆动模块已完成 + keDeviceWorkStatus_Device_Auto_Stop, //< 设备自动停止 + keDeviceWorkStatus_EyeCB_TaskFull, //< 任务饱和 + keDeviceWorkStatus_EyeCB_GetImageErr, //< 获取图像错误 + + keDeviceWorkStatus_EyeCB_ObjectCome, //< 物体来了 + keDeviceWorkStatus_EyeCB_ObjectLeave, //< 物体走了 + + keDeviceWorkStatus_Trigger_Coming, //< Trigger信号来了 + keDeviceWorkStatus_Trigger_Leave, //< Trigger信号结束了 + + keDeviceWorkStatus_Eye_StreamResume, //< 数据恢复 + keDeviceWorkStatus_Eye_SwingNoResponse, //< 静态模块无响应 + + keDeviceWorkStatus_PetaMsg, //< Peta 消息 + keDeviceWorkStatus_PSMsg, //< PS 消息 + keDeviceWorkStatus_Eye_LaserlineErr, //< + keDeviceWorkStatus_Device_Swing_Running, //< 开流后摆动模块开始运动时给出信号 + + keDeviceWorkStatus_Eye_SwingEncodeNoChange, //< 摆动编码器值不改变 + keDeviceWorkStatus_Eye_SwingUpdateStatus, //< 发现了摆动模块 + + keDeviceWorkStatus_Eye_New_Client_Connected, //< 有新客户端连接 + + keDeviceWorkStatus_Eye_LinearActuator_Warning, //< 推杆电机关闭点可能存在异常 +} EVzDeviceWorkStatus; + +/// @brief +/// 设备状态 +typedef enum +{ + keMulDevWorkStatusUnknown = 0, //< 未知 + keMulDevWorkStatus_Auto_Stop, //< 自动停止 +} EVzMulDevWorkStatus; + +/// @brief +/// Laser FrameRate +typedef enum +{ + keLaserFrameRate_30 = 0, + keLaserFrameRate_100, + keLaserFrameRate_220, + keLaserFrameRate_400, + keLaserFrameRate_630, + keLaserFrameRate_50, + keLaserFrameRate_120, + keLaserFrameRate_320, + keLaserFrameRate_550, + keLaserFrameRate_830, + keLaserFrameRate_80, + keLaserFrameRate_210, + keLaserFrameRate_530, + keLaserFrameRate_840, + keLaserFrameRate_1200, + keLaserFrameRateLast +} EVzLaserFrameRate; + +/// @brief +/// Laser FrameRate +typedef enum +{ + keLaserCaptureMode_LastFrame, + keLaserCaptureMode_AllFrame, + keLaserCaptureMode_CalcAllFrameWhenStop, +} EVzLaserCaptureMode; + +/// @brief +/// 产品类型 +enum +{ + keProjectType_None = 0, + keProjectType_Coal, + keProjectType_Logistics, + keProjectType_InfraredMark, + keProjectType_DivRebar, + keProjectTypeLast, +}; +typedef unsigned int EVzProjectType; + +/// @brief +/// 定义检测物体类型 +enum +{ + keVzNLDetectObjType_Unknown = 0, + keVzNLDetectObjType_Line = 1, // 直线 + keVzNLDetectObjType_CoplaneCircle = 2, // 圆/椭圆(共面) + keVzNLDetectObjType_Triangle = 3, + keVzNLDetectObjType_Rect = 4, // 矩形 + keVzNLDetectObjType_Cube = 5, // 立方体 + keVzNLDetectObjType_StickWidth = 6, // 杆状物 + keVzNLDetectObjType_StickLength = 7, + keVzNLDetectObjType_StickVolume = 8, + keVzNLDetectObjType_StickApex = 9, + keVzNLDetectObjType_Laser = 10, // 激光 + keVzNLDetectObjType_Slot = 11, // 凹槽 + keVzNLDetectObjType_SameSizeCircle = 12, // 同大小 + keVzNLDetectObjType_3DModel = 14, // 匹配三维模型 +}; +typedef unsigned int EVzNLDetectObjType; + +enum +{ + keSDKType_Unknown = 0, + keSDKType_DetectShape, + keSDKType_DetectLaser, + keSDKType_DetectCircle, + keSDKType_DetectColorMark, + keSDKType_DetectGrains, +}; +typedef int EVzSDKType; + +typedef enum +{ + keVzColorType_None = 0, + keVzColorType_Red, + keVzColorType_Green, + keVzColorType_Blue, + keVzColorType_Last, +} EVzColorType; + +/// @brief +/// Log Level +enum +{ + keNLLogLevel_None = 0, + keNLLogLevel_Error, + keNLLogLevel_Warning, + keNLLogLevel_Debug, + keNLLogLevel_Information, +}; +typedef int EVzNLLogLevel; + +/// @brief +/// Log Type +enum +{ + keNLLogType_None = 0, + keNLLogType_Console = 0x01, + keNLLogType_File = 0x02, + keNLLogType_LocalPgm= 0x04, + keNLLogType_Remote = 0x08, +}; +typedef int EVzNLLogType; + +/* +* @brief +* 眼睛配置信息 +*/ +typedef enum +{ + keNLEthernetEyeIPType_Unknown = 0, //< 未知 + keNLEthernetEyeIPType_StaticIP, //< 静态IP + keNLEthernetEyeIPType_DHCP, //< DHCP +} EVzNLEthernetEyeIPType; + +/// @brief +/// 爆光模式 +typedef enum +{ + keVzNLExposeMode_Fix = 0, ///< 定值曝光 + keVzNLExposeMode_Auto = 1, ///< 自动曝光 +} EVzNLExposeMode; + +/// @brief +/// Expose Type +typedef enum +{ + keExposeThresType_Average = 0, + keExposeThresType_Max = 1, +} EVzExposeThresType; + +/// @brief +/// 图像压缩格式 +typedef enum +{ + keVzNLImageCompress_Unknown = 0, + keVzNLImageCompress_Jpeg = 1, // Jpeg格式 + keVzNLImageCompress_Png = 2, // png格式 + keVzNLImageCompress_Bmp = 3 // bmp格式 +} EVzNLImageCompress; + +/// @brief +/// 图像类型 +typedef enum +{ + keVzNLImageType_None = 0, + keVzNLImageType_RGB888, // 3 channel 24bit + keVzNLImageType_BGR888, // 3 channel 24bit + keVzNLImageType_BAYERGR, // 1 channel + keVzNLImageType_BAYERRG, // 1 channel + keVzNLImageType_BAYERBG, // 1 channel + keVzNLImageType_BAYERGB, // 1 channel + keVzNLImageType_GRAY, // 1 channel + keVzNLImageType_YUV420, + keVzNLImageType_BGRA8888, // 4 channel 32bit + keVzNLImageType_YUYV_YUV422,// + keVzNLImageType_UNKNOWN = 0xffff, +} EVzNLImageType; + +/// @brief +/// 圆形定义 +typedef enum +{ + keNL3DCircleTypeUnknown = 0, + keNL3DCircleType_Circle = 3, // 单圆 + keNL3DCircleType_ConcentricCircle = 4, // 同心圆 + keNL3DCircleType_SteelTube = 5, // 钢管 +} EVzNL3DCircleType; + +/// @brief +/// 眼睛数据工作模式 +typedef enum +{ + keNLEyeDataWorkMode_Data = 0, // 数据模式 + keNLEyeDataWorkMode_ImageData = 1, // 图像数据模式 + keNLEyeDataWorkMode_Image = 2, +} EVzNLEyeDataWorkMode; + +/// @breif +/// 数据翻转 +enum +{ + keFlipType_None = 0x00, ///< 无翻转 + keFlipType_Vertical = 0x01, ///< 垂直翻转 +}; +typedef unsigned int EVzFlipType; + +/// @brief +/// 激光点过滤类型 +enum +{ + keLaserPointFilterType_None = 0, ///< 不过滤 + keLaserPointFilterType_BetweenYMinMax = 0x01, ///< 保留Y轴某范围内的点 + keLaserPointFilterType_ExceptYRange = 0x02, ///< 排除Y轴某范围 + keLaserPointFilterType_BetweenZMinMax = 0x10, ///< 保留Z轴某范围的点 + keLaserPointFilterType_ExceptZRange = 0x20, ///< 排除Z轴某范围的点 + keLaserPointFilterType_BetweenXMinMax = 0x100, ///< 保留X轴某范围的点 + keLaserPointFilterType_ExceptXRange = 0x200 ///< 排除X轴某范围的点 +}; + +typedef unsigned int EVzLaserPointFilterType; + +// @brief +// 摆动机构扫描模式 +typedef enum +{ + keSwingMotorScanModeNone = 0, + keSwingMotorScanMode_Once, //< 一次 + keSwingMotorScanMode_Loop, //< 轮询 +} EVzSwingMotorScanMode; + +// @brief +// 灰度值来源 +typedef enum +{ + keGrayValueSource_Left = 0, //< 左目 + keGrayValueSource_Right, //< 右目 + keGrayValueSource_Both, //< 左右 +} EVzGrayValueSource; + +// @brief +// 摆动机构速度 +const float keSwingMotorSpeedMin = 1.f; +const float keSwingMotorSpeed_12 = 12.f; +const float keSwingMotorSpeed_24 = 24.f; +const float keSwingMotorSpeed_36 = 36.f; +const float keSwingMotorSpeed_48 = 48.f; +const float keSwingMotorSpeed_60 = 60.f; +const float keSwingMotorSpeed_72 = 72.f; +const float keSwingMotorSpeed_84 = 84.f; +const float keSwingMotorSpeed_96 = 96.f; +const float keSwingMotorSpeedMax = keSwingMotorSpeed_96; + +// @brief +// 参考ROI大小 +typedef enum +{ + keDeviceROIInvalid = 0, + keDeviceROI_128_1024, //< ROI 128 * 1024 + keDeviceROI_256_1024, //< ROI 256 * 1024 + keDeviceROI_Full, //< ROI Full Resolution + keDeviceROILast //< +} EVzDeviceROI; + +/// @brief +/// 摆动机构旋转方向 +typedef enum +{ + keSwingRotateDirect_Clockwise, //< 顺时针旋转 + keSwingRotateDirect_Counterclockwise //< 逆时针旋转 +} EVzSwingRotateDirect; + +/// @brief +/// 图像分辨率 +typedef struct +{ + unsigned int nFrameWidth; + unsigned int nFrameHeight; +} SVzVideoResolution; + +/// @brief +/// 点类型 +typedef enum +{ + keResultDataType_Invalid = -1, // 3D数据类型 2D数据类型 + keResultDataType_Position, //< Position数据类型 使用SVzNL3DPosition SVzNL2DPosition + keResultDataType_PointF, //< PointF数据类型 使用SVzNL3DPointF SVzNL2DLRPoint + keResultDataType_PointXYZ, //< PointXYZ数据类型 使用SVzNLPointXYZ SVzNL2DLRPoint + keResultDataType_PointXYZRGBA, //< PointXYZRGB数据 使用SVzNLPointXYZRGBA SVzNL2DLRPoint + keResultDataType_PointGray, //< PointXYZGray数据 使用SVzNLPointXYZGray SVzNL2DLRPoint + keResultDataType_PointXYZI, //< PointXYZI数据 使用SVzNLPointXYZI SVzNL2DLRPoint + keResultDataType_PointRGBA_D, //< PointRGBA_D 使用SVzNLPointRGBA_D SVzNL2DLRPoint + keResultDataType_PositionF, //< Position数据类型 使用SVzNL3DPosition SVzNL2DPositionF +} EVzResultDataType; + +/// @brief 设备参数类型 +typedef enum +{ + keDeviceParamType_Invalid = 0, //< 无效 + keDeviceParamType_FrameRate, //< 帧率参数 + keDeviceParamType_Expose, //< 曝光参数 + keDeviceParamType_Gain, //< 增益参数 + keDeviceParamType_LaserLight, //< 激光亮度参数 + keDeviceParamType_FrameRateWithoutColorData,//< 不携带颜色数据的帧率 + keDeviceParamType_FrameRateWithColorData, //< 携带颜色数据的帧率 + keDeviceParamType_StrobeTimeRatio, //< C版激光器亮度值 + keDeviceParamType_RGBExpose, //< RGB曝光参数 + keDeviceParamType_RGBGain, //< RGB增益参数 +} EVzDeviceParamType; + +/// @brief 设备参数类型 +typedef enum +{ + keDevChipInfoInvalid = 0, + keDevChipInfo_Temperature, + keDevChipInfo_VccPin, + keDevChipInfo_VccPaux, + keDevChipInfo_VccPdro, +} EVzDevChipInfo; + +/// @brief +/// 网络信息 +typedef struct +{ + unsigned int nAdapterIndex; //< 网络适配器索引 + char szNetCardName[VZ_MAXPATH]; //< 网卡名称 + unsigned char byLocalIP[4]; //< 本机网卡IP + unsigned char byMacAddress[6]; //< 本机网卡MAC + unsigned char bySubMask[4]; //< 子网掩码 + unsigned char byGetWay[4]; //< 网关 + unsigned int dwSpeed; //< 网络速度 Mbps +} SVzNetCardInfo; + +/// @brief +/// 眼睛控制板的设备信息 +typedef struct +{ + unsigned int nSize; // 结构大小 + EVzDeviceType eDeviceType; // 设备类型 + char szDeviceName[VZNL_DEVICE_NAME_LENGTH]; // 名称 + char byServerIP[VZNL_SDK_NETWORK_IPv4_LENGTH]; // 服务端IP + char byMAC[VZNL_SDK_NETWORK_MAC_LENGTH]; // MAC地址 + char szGUID[VZNL_GUID_LENGTH]; // 唯一标识 + char szDeviceID[VZNL_GUID_LENGTH]; // 设备ID + char szDesc[VZ_DEVICE_DESC]; // 设备描述 + VzBool bValidDevice; // Device是否有效 +} SVzNLEyeCBInfo; + +/// @brief +/// 控制板信息 +typedef struct +{ + SVzNLEyeCBInfo sEyeCBInfo; //< Base Info + unsigned int nHardwareVersion; //< Device HW Version + unsigned int chEyeVersion; //< Device Eye Version + char chEyeName[VZ_DEVICE_NAME]; //< Device Eye Name + char chEyeIPv4[VZNL_SDK_NETWORK_IPv4_LENGTH];//< Device IP Address + char chEyeMAC[VZNL_SDK_NETWORK_MAC_LENGTH]; //< Device Eye MAC + char szDeviceAddr[VZNL_DEVICE_ADDR_LENGTH]; //< Device Address + SVzVideoResolution sVideoRes; //< Device Video Resolution + char chHwSerialID[VZNL_DEVICEID_LENGTH]; //< Device ID Length + +} SVzNLEyeCBDeviceInfoEx; + +/// @brief 设备能力描述 +typedef union +{ + unsigned int nCapabilityMask; + struct + { + unsigned int bIsSupportRGBSensor : 1; //< 是否支持Center Sensor + unsigned int bSupportSwingMotor : 1; //< 是否支持摆动机构 + unsigned int bIsSupportAsyncRGBD : 1; //< 是否支持异步RGBD + }; + +} SVzEthLaserEyeCapability; + +/// @brief +/// 眼睛信息 +typedef struct +{ + SVzNLEyeCBInfo sEyeCBInfo; + EVzEyeTypeFlag eEyeTypeFlag; //< 眼睛类型标志 + SVzVideoResolution sVideoRes; //< 眼睛分辨率 + int nFixDisparity; //< 偏心 + SVzEthLaserEyeCapability sCapability; + unsigned int nMaxFrameIdx; //< 帧号最大值 + unsigned int nMaxTimeStamp; //< 时间戳最大值 + int nHardwareVersion; //< 硬件版本 + char szDeviceAddr[VZNL_DEVICE_ADDR_LENGTH]; //< Device Address + char szVersion[VZNL_VERSION_LENGTH]; //< Version + + // Ethernet Info + EVzNLEthernetEyeIPType eEthernetIPType; //< 以太网IP类型 + char chEyeIP[VZNL_SDK_NETWORK_IPv4_LENGTH]; //< 眼睛IP + char chEyeMac[VZNL_SDK_NETWORK_MAC_LENGTH]; //< 当前眼睛MAC + char chLocalIP[VZNL_SDK_NETWORK_IPv4_LENGTH];//< 网卡IP + char chLocalMac[VZNL_SDK_NETWORK_MAC_LENGTH];//< 网卡MAC + char vendor[8]; //< + char productName[8]; //< 产品名 + char serialID[8]; //< 序列号 + + // Ext Data + unsigned int nSDKClientCnt; //< SDK Link Count + unsigned int nModbusClientCnt; //< Modbus Link Count + unsigned int nEthernetSpeed; //< xxx Mbps + char szHardwareVersion[32]; //< HardWareVersion + unsigned int nUdpFlag; //< 用于判断是否可以使用UDP广播修改IP,0表示不可以 1表示可以 + unsigned int nPupilDistance; //< 瞳距 + + // NetCard Info + SVzNetCardInfo sNetCardInfo; //< 网卡信息 +} SVzNLEyeDeviceInfoEx; + +/// @brief 系统配置参数 +typedef struct +{ + unsigned int nDeviceTimeOut; // 设备超时设置 +} SVzNLConfigParam; + +/// @brief 打开设备类型 +typedef struct _tagSVzNLOpenDeviceParam +{ + EVzNLEyeResolution eEyeResolution; // 眼睛的分辨率 + EVzSDKType eSDKType; + VzBool bStopStream; + _tagSVzNLOpenDeviceParam() + { + eEyeResolution = keVzNLEyeResolution_Unknown; + eSDKType = keSDKType_Unknown; + bStopStream = VzTrue; + } +} SVzNLOpenDeviceParam; + +typedef struct +{ + char szPath[256]; +} SVzNLTestOpenDeviceParam; + +/// @brief +/// 更新工程信息 +typedef struct +{ + char szProject[32]; + char szVersion[32]; +} SVzSystemProjectInfo; + +/// @breif +/// 版本信息。 +typedef struct +{ + long bValidSDKVersion: 1; ///< SDK版本是否可用 + long bValidAppVersion: 1; ///< EyeCB版本是否可用 + long bValidCameraVersion: 1; ///< 眼睛库版本是否可用 + long bValidAlgoVersion: 1; ///< 算法版本是否可用 + long bValidApkVersion: 1; ///< 安卓版本是否可用 + long bValidDevIDVersion: 1; ///< DEVID版本是否可用 + long bValidWifiVersion: 1; ///< WIFI版本是否可用 + long bValidHDMIVersion: 1; ///< HDMI版本是否可用 + long bValidVizumEyeVersion : 1; ///< 硬件版本是否可用 + long bValidFwVersion : 1; ///< 硬件版本是否可用 +} SVzNLVersionCapGroup; + +typedef union +{ + SVzNLVersionCapGroup sVersionCap; + long nMaskCap; +} SVzNLDevVersionCap; + +/// @brief +/// 版本信息 +typedef struct +{ + SVzNLDevVersionCap sCap; + char szSDKVersion[VZNL_VERSION_LENGTH]; ///< SDK版本 + char szAppVersion[VZNL_VERSION_LENGTH]; ///< App版本 + char szCameraVersion[VZNL_VERSION_LENGTH]; ///< 眼睛版本 + char szAlgoVersion[VZNL_VERSION_LENGTH]; ///< 算法版本 + char szApkVersion[VZNL_VERSION_LENGTH]; ///< 安卓版本 + char szDevIDVersion[VZNL_VERSION_LENGTH]; ///< 设备ID版本 + char szWifiVersion[VZNL_VERSION_LENGTH]; ///< WIFI版本 + char szHDMIVersion[VZNL_VERSION_LENGTH]; ///< HDMI版本 + char szVizumEyeVersion[VZNL_VERSION_LENGTH];///< 硬件版本 + char szFwVersion[VZNL_VERSION_LENGTH]; ///< 硬件版本 +} SVzNLVersionInfo; + +/// @brief +/// 图像信息 +typedef struct +{ + unsigned char* pBuffer; // 图像buffer信息 + unsigned int nBufferSize; // 图像buffer的大小 + + unsigned int nWidth; // 图像宽度 + unsigned int nHeight; // 图像高度 + unsigned char nChannels; // 通道数 + EVzNLImageCompress eImageCompress; +} SVzNLCompressData; + +/// @brief +/// 点坐标 +typedef struct +{ + int x; + int y; +} SVzNL2DPoint; + +/// @brief +/// 点坐标 +typedef struct +{ + unsigned short x; + unsigned short y; +} SVzNL2DPointShortU; + +/// @brief +/// 点坐标 +typedef struct +{ + SVzNL2DPoint sLeft; + SVzNL2DPoint sRight; +} SVzNL2DLRPoint; + +/// @brief +/// 点坐标 +typedef struct +{ + float x; + float y; +} SVzNL2DPointF; + +typedef struct +{ + double x; + double y; +} SVzNL2DPointD; + +typedef struct +{ + double x; + double y; + double z; +} SVzNL3DPoint; + +typedef struct +{ + float x; + float y; + float z; +} SVzNL3DPointF; + +// 3D Point +typedef union +{ + float fData[4]; + struct + { + float x; + float y; + float z; + }; +} SVzNLPointXYZ; + +// 3D Point + RGB +typedef union +{ + float fData[4]; + struct + { + float x; + float y; + float z; + VzColorRGB nRGB; + }; +} SVzNLPointXYZRGBA; + +// 3D Point + Gray +typedef union +{ + float fData[4]; + struct + { + float x; + float y; + float z; + int nGray; + }; +} SVzNLPointXYZGray; + +// 3D Point + Gray +typedef struct +{ + union + { + float fData[4]; + struct + { + float x; + float y; + float z; + }; + }; + + union + { + float fData_c[4]; + struct + { + float intensity; + }; + }; +} SVzNLPointXYZI; + +// 3D Point + Gray +typedef struct +{ + union + { + double dData[4]; + struct + { + double x; + double y; + double z; + VzColorRGB nRGB; + }; + }; +} SVzNLPointRGBA_D; + +typedef struct +{ + int x; + int y; + int z; + int flag; +} SVzNLLogisticsPoint; + +typedef struct +{ + float x; + float y; + float z; + float reserve; +} SVzNLAlign3DPointF; + +typedef struct +{ + int x; + int y; + int z; + int reserve; +} SVzNLAlign3DPointI; + +struct SVz3DPointFWithPixel +{ + float x; + float y; + float z; + unsigned char byPixelL; + unsigned char byPixelR; + unsigned char byReserve[2]; +}; + +/// @brief 范围类型 double +typedef struct +{ + float min; //< 最小值 + float max; //< 最大值 +} SVzNLRangeF; + +/// @brief 范围类型 double +typedef struct +{ + double min; //< 最小值 + double max; //< 最大值 +} SVzNLRangeD; + +/// @brief 范围类型 int +typedef struct +{ + unsigned int nMin; //< 最小值 + unsigned int nMax; //< 最大值 +} SVzNLRangeU; + +/// @brief 范围类型 int +typedef struct +{ + int nMin; //< 最小值 + int nMax; //< 最大值 +} SVzNLRange; + +/// @brief 3D值范围 +typedef struct +{ + SVzNLRangeD xRange; //< X范围 + SVzNLRangeD yRange; //< Y范围 + SVzNLRangeD zRange; //< Z范围 +} SVzNL3DRangeD; + +/// @brief 线的2D信息 +typedef struct +{ + SVzNL2DPoint ptStart2D; // 2D起始坐标 + SVzNL2DPoint ptEnd2D; // 2D结束坐标 +} SVzNL2DLine; + +typedef struct +{ + SVzNL2DPointF ptStart2D; // 2D起始坐标 + SVzNL2DPointF ptEnd2D; // 2D结束坐标 +} SVzNL2DLineF; + +/// @brief 线的3D信息 +typedef struct +{ + SVzNL3DPoint ptStart3D; // 3D起始坐标 + SVzNL3DPoint ptEnd3D; // 3D结束坐标 +} SVzNL3DLine; + +/// @brief 2D矩形 +typedef struct +{ + SVzNL2DPoint ptLT; //< 左上 + SVzNL2DPoint ptRT; //< 右上 + SVzNL2DPoint ptRB; //< 右下 + SVzNL2DPoint ptLB; //< 左下 +} SVzNL2DRect; + +/// @brief 3D矩形 +typedef struct +{ + SVzNL3DPoint ptLT; //< 左上 + SVzNL3DPoint ptRT; //< 右上 + SVzNL3DPoint ptRB; //< 右下 + SVzNL3DPoint ptLB; //< 左下 +} SVzNL3DRect; + +/// @brief 三角形 +typedef struct +{ + SVzNL3DPoint ptVertexA; + SVzNL3DPoint ptVertexB; + SVzNL3DPoint ptVertexC; +} SVzNL3DTriangle; + +/// @brief 三角形 +typedef struct +{ + SVzNL2DPoint ptVertexA; + SVzNL2DPoint ptVertexB; + SVzNL2DPoint ptVertexC; +} SVzNL2DTriangle; + +/// @brief 点的坐标 +typedef struct +{ + int nPointIdx; // 点序 + SVzNL2DPoint ptLeft2D; // 2D左图坐标 + SVzNL2DPoint ptRight2D; // 2D右图坐标 + SVzNL3DPoint pt3D; // 3D坐标 +} SVzNLPosition; + +/// @brief 点坐标 (double) +typedef struct +{ + int nPointIdx; // 点序 + SVzNL2DPointD ptLeft2D; // 2D左图坐标 + SVzNL2DPointD ptRight2D; // 2D右图坐标 + SVzNL3DPoint pt3D; // 3D坐标 +} SVzNLPositionD; + +/// @brief 2D点坐标 +typedef struct +{ + int nPointIdx; // 点序 + SVzNL2DPoint ptLeft2D; // 2D左图坐标 + SVzNL2DPoint ptRight2D; // 2D右图坐标 +} SVzNL2DPosition; + +/// @brief 2D点坐标 +typedef struct +{ + int nPointIdx; // 点序 + SVzNL2DPointF ptLeft2D; // 2D左图坐标 + SVzNL2DPointF ptRight2D; // 2D右图坐标 +} SVzNL2DPositionF; + + +/// @brief 3D点坐标 +typedef struct +{ + int nPointIdx; // 点序 + SVzNL3DPoint pt3D; // 3D坐标 +} SVzNL3DPosition; + +/// @brief 3D激光线 +typedef struct +{ + SVzNL3DPosition* p3DPosition; //< 3D点(数组) + int nPositionCnt; //< 3D点个数 + unsigned long long nTimeStamp; //< 时间戳 +} SVzNL3DLaserLine; + +/// @brief 3D激光线 +typedef struct +{ + SVzNLPointXYZRGBA* p3DPoint; //< 3D点(数组) + int nPointCnt; //< 3D点个数 + unsigned long long nTimeStamp; //< 时间戳 +} SVzNLXYZRGBDLaserLine; + +/// @brief 3D点坐标(float) +typedef struct +{ + int nPointIdx; //< 点索引 + SVzNL3DPointF pt3D; //< 点值 +} SVzNL3DPositionF; + +/// @brief 矩形 +typedef struct +{ + int left; //< 左 + int right; //< 右 + int top; //< 上 + int bottom; //< 下 +} SVzNLRect; + +/// @brief 大小 +typedef struct +{ + int nWidth; //< 宽 + int nHeight; //< 高 +} SVzNLSize; + +/// @brief 大小 +typedef struct +{ + short shWidth; //< 宽 + short shHeight; //< 高 +} SVzNLSizeSh; + +/// @brief 大小 +typedef struct +{ + float fWidth; //< 宽 + float fHeight; //< 高 +} SVzNLSizeF; + +/// @brief 大小(double) +typedef struct +{ + double dWidth; //< 宽 + double dHeight; //< 高 +} SVzNLSizeD; + +/// @brief 三角形的类型 +typedef struct +{ + double dSideA; + double dSideB; + double dSideC; +} SVzNLTriangleInfo; + +/// @brief +/// ROI区域 +/// o————————————————o +/// | ↑ ↑ | +/// | |T |B | +/// | ↓ | | +/// | L o——|——o | +/// |←———→| | | | +/// | R | | | | +/// |←————|——↓—→| | +/// | o—————o | +/// o————————————————o +typedef struct +{ + int left; // 左 + int right; // 右 + int top; // 上 + int bottom; // 下 +} SVzNLROIRect; + +/// @brief 2D 位置 +typedef struct +{ + double x; //< X + double y; //< Y + double value; //< None +} SVzNL2DValueD; + +/// @brief 设备范围数据 +typedef struct +{ + SVzNLRangeU sFrameRate; //< 帧率范围 + SVzNLRangeU sExpose; //< 曝光范围 + SVzNLRangeU sGain; //< 增益范围 +} SVzDeviceLimitInfo; + +/// @brief 图像信息 +typedef struct +{ + unsigned char* pBuffer; // 图像buffer信息 + unsigned int nBufferSize; // 图像buffer的大小 + + SVzNL2DPoint ptOriPos; // 原始位置(顶点) + unsigned int nWidth; // 图像宽度 + unsigned int nHeight; // 图像高度 + EVzNLImageType eImageType; // 图像格式 + unsigned char byBitDepth; // 位图深度 + unsigned char nChannels; // 通道数 +} SVzNLImageData; + +//--------------------------------------------------------------- +// @brief 3D数据二维索引表 +//--------------------------------------------------------------- +typedef struct +{ + EVzResultDataType ePointType; // 3D点类型 + void* pPointPtr; // 3D点指针 + unsigned int nWidth; // 二维表宽 + unsigned int nHeight; // 二维表高 +} SVz3DPointMapTable; + +/// @breif 各检测类型的结构体 +typedef struct +{ + EVzNLDetectObjType eDetectType; + int nReserve[2];//< 保留指针,可能是内部的一个对象? +} SVzNLDetectObjectResult; + +/// @breif 线的2D结果 +typedef struct +{ + SVzNL2DLine sLeftLine; + SVzNL2DLine sRightLine; + double dLineLength; // 线长 +} SVzNL2DLineResult; + +/// @breif 线的3D结果 +typedef struct +{ + SVzNL3DLine sLine; + double dLineLength; // 线长 +} SVzNL3DLineResult; + +/// @breif 线的2D结果 +typedef struct +{ + SVzNL2DRect sLeftRect; + SVzNL2DRect sRightRect; + + double dTopDistance; + double dRightDistance; + double dBottomDistance; + double dLeftDistance; +} SVzNL2DRectResult; + +/// @breif 2D结果 +typedef struct +{ + SVzNL3DRect sRect; + double dTopDistance; + double dRightDistance; + double dBottomDistance; + double dLeftDistance; +} SVzNL3DRectResult; + +/// @breif 三角形结果 +typedef struct +{ + SVzNL2DTriangle sLeftTriangle; + SVzNL2DTriangle sRightTriangle; + + double dABDistance; + double dBCDistance; + double dACDistance; +} SVzNL2DTriangleResult; + +typedef struct +{ + SVzNL3DTriangle sTriangle; + + double dABDistance; + double dBCDistance; + double dACDistance; +} SVzNL3DTriangleResult; + +/// @brief 椭圆检测结果 +typedef struct +{ + SVzNLDetectObjectResult sDetectObj; + SVzNL2DPoint s2DPoint; +} SVzNLDetectEllipseResult; + +typedef struct +{ + int nSize; +} SVzNLDetectCubeParam; + +typedef struct +{ + SVzNLDetectObjectResult sDetectObj; +} SVzNLDetectCubeResult; + +/// @brief 3D物体的连接线 +typedef struct +{ + int nLineStart; + int nLineEnd; +} SVzNL3DObjectLine; + +/// @brief 像素深度值归一化模式 +typedef enum +{ + keDepthNormalizingMode_None = 0, //不使用归一化 + keDepthNormalizingMode_Indicated = 1, //指定固定归一化范围 + keDepthNormalizingMode_Auto = 2 //自动检测归一化范围 +} EVzDepthNormalizingMode; + +/// @brief 图像位深 +typedef enum +{ + keImageBitDepthTypeNone = 0, + keImageBitDepthType_8bit = 8, + keImageBitDepthType_16bit = 16, +} EVzImageBitDepthType; + +/// 深度图存储配置参数 +typedef struct +{ + EVzDepthNormalizingMode eDepthNormalizingMode; // 像素深度值归一化模式 + unsigned int nImageMargin; // 输出深度图边缘预留空白区域宽度(单位:像素值) + EVzImageBitDepthType eBitDepth; // 输出深度图的位深度: + float fDepthResolution; // 深度图中深度值精度,例如:0.1 表示1个像素值对应0.1mm 实际距离 + + float fWHPixelResolution; // 深度图中每个像素的精度,例如:0.1 表示1个像素点对应0.1mm实际距离,eDepthNormalizingMode = eDepthNormalizingMode_Indicated 时有效 + float fNormalizingStartVal; // 客户指定的归一化范围的起始值(mm):eDepthNormalizingMode = keDepthNormalizingMode_Indicated 时有效 + float fNormalizingEndVal; // 客户指定的归一化范围的结束值(mm):eDepthNormalizingMode = keDepthNormalizingMode_Indicated 时有效 + + float fNormalizingMargin; // eDepthNormalizingMode = keDepthNormalizingMode_Auto 时,在自动检测到的归一化区间上下预留的空白空间高度(单位mm)上下各预留指定高度空间 + +} SVzDepthMapImgCtlPara; + +/// @brief 3D物体面信息 +typedef struct +{ + int nPointCount; + unsigned int* pArryPointIdx; +} SVzNL3DObjectPanel; + +/// @brief 三维物体描述 +typedef struct +{ + int nType; + SVzNL3DPoint* pGrabPoint; ///< 物体抓取点 + int nGrabPointCount; + SVzNL3DPoint* p3DPoint; ///< 物体各顶点 + int n3DPointCount; ///< 物体上点的个数 + SVzNL3DObjectLine* p3DObjLine; ///< 物体上的线 + int n3DObjLineCount; ///< 物体上线的个数 + SVzNL3DObjectPanel* p3DObjPanel; + int n3DObjPanelCount; +} SVzNL3DObjectDesc; + +/// @brief 三维物体描述 +typedef struct +{ + SVzNL2DPoint* pLeftGrabPoint; ///< 物体抓取点 + SVzNL2DPoint* pRightGrabPoint; ///< 物体抓取点 + int nGrabPointCount; + SVzNL2DPoint* pLeft2DPoint; ///< 物体各顶点 + SVzNL2DPoint* pRight2DPoint; ///< 物体各顶点 + int n2DPointCount; ///< 物体上点的个数 + SVzNL3DObjectLine* p3DObjLine; ///< 物体上的线 + int n3DObjLineCount; ///< 物体上线的个数 + SVzNL3DObjectPanel* p3DObjPanel; + int n3DObjPanelCount; +} SVzNL3DObject2DResult; + +/// @brief ColorMark 形状 +typedef enum +{ + keVzNLColorMarkType_Unknown = 0, + keVzNLColorMarkType_Rect, + keVzNLColorMarkType_Triangle, + keVzNLColorMarkType_Circle, +} EVzNLColorMarkType; + +/// @brief Color Mark +typedef struct +{ + int nPointIdx; // 点序 + SVzNL3DPoint pt3D; // 3D坐标 +} SVzNLColorMark3DResult; + +/// @brief ROI +typedef struct +{ + int nPointIdx; // 点序 + SVzNL2DPoint ptLeft2D; // 2D左图坐标 + SVzNL2DPoint ptRight2D; // 2D右图坐标 + SVzNLRect sLeftBox; + SVzNLRect sRightBox; +} SVzNLColorMark2DResult; + +/// @brief ROI +typedef struct +{ + int nPointIdx; // 点序 + EVzNLColorMarkType eType; + SVzNL3DPoint pt3D; + SVzNL2DPoint ptLeft2D; // 2D左图坐标 + SVzNL2DPoint ptRight2D; // 2D右图坐标 + SVzNLRect sLeftBox; + SVzNLRect sRightBox; +} SVzNLColorMarkResult; + +typedef struct +{ + SVzNL3DPosition* p3DPoint; + int nCount; +}SVzDetectGrainsLaserResult; + +typedef struct +{ + unsigned int nIndex; + float fLength; + float fWidth; + float fHeight; + + float fVolume; //until mm + SVzNL3DPoint sObjCenter; + SVzNL3DPoint sBoxGoundTop[4]; + SVzNL3DPoint sBoxGoundBottom[4]; +} SVzLaserGrainsData; + +typedef struct +{ + SVzLaserGrainsData* pGrainData; + int nCount; +} SVzDetectGrainResult; + +typedef enum +{ + keGrainLaserStatus_Unknown = 0, + keGrainLaserStatus_BeginWork, + keGrainLaserStatus_Working, + keGrainLaserStatus_EndWork, +} EVzGrainLaserStatus; + +///< 每个石子的信息 +typedef struct +{ + int index_; + char lithology[128]; //岩性 + char origin_[128]; //产地 + double length_; //长 + double width_; //宽 + double height_; //高 + double vol_; //体积 + double area_; //面积 + double perimeter_; //周长 + float A_; //Axial factor 轴向系数 + float CR_; //Convexity 凸度 + float AP_; //棱角参数 + VzBool isNeedle_; //是否针片状 + SVzNLRect rcPos; +} SVzLaserAggregateData; + +///< 级配信息 +typedef struct +{ + int index_; // 级配级别 6 7 8 9 10 11 12 13 + int num_; // 数量 + float pi_; // pi + float passRate_; // 通过率 + float top_; // 上限 + float bottom_; // 下限 +} SVzLaserAggregateLevelData; + +///< 石子检测综合信息 +typedef struct +{ + char chLithology[128]; ///< 岩性 + char chOrigin[128]; ///< 产地 + float fNeedleRate; ///< 针片状含量 + float fMeanAP; ///< 综合棱角值 + float fMeanA; ///< 综合轴向系数 + float fMeanCR; ///< 综合凸度 +} SVzLaserAggregateMeanInfo; + +///< 石子检测状态 +typedef enum +{ + keAggerateLaserStatus_Unknown = 0, + keAggerateLaserStatus_BeginWork, ///< 开始工作 + keAggerateLaserStatus_Working, ///< 正在工作 + keAggerateLaserStatus_EndWork, ///< 结束工作 +} EVzAggerateLaserStatus; + +/// @brief +/// 钢筋信息 +typedef struct +{ + int nFlag; + int nRadius; + float fPhyRadius; + SVzNL2DPoint nPoint; + int nLineIndex; + unsigned int nTimeStamp; + SVzNL3DPoint n3DPoint; +}SVzNLCounterItem; + +/// @brief +/// 钢筋信息 +typedef struct +{ + int nNum; // 总数 + SVzNLCounterItem* pPointArr; // 二维扫描图坐标 + unsigned long long nStartTimeStamp;// 起始线的时间戳 + SVzNLPosition sTopPoint; // 最突出点数据 + SVzNLCounterItem sMarkPoint; // mark信息 + SVzNLImageData sImage; // 生成的二维图像 +} SVzNLCounter; + +/// @brief +typedef struct +{ + unsigned int nSize; + char szName[64]; + char szVersion[64]; + void* pVoid; + int nVersionCode; +} SVzAICreateParam; + +/** + * @brief + * 输出帧参数 + */ +typedef struct +{ + unsigned long long nFrameIdx; //< 帧号 + unsigned long long nTimeStamp; //< 时间戳 +} SVzOutputFrameProps; + +/** + * @brief + * Circle Mark Information + */ +typedef struct +{ + int nMarkID; //< Mark 标志 + SVzNL3DPosition ptCenterPos; //< 中心点 + SVzNL3DPosition ptLeftPos; //< 左 + SVzNL3DPosition ptTopPos; //< 上 + SVzNL3DPosition ptRightPos; //< 右 + SVzNL3DPosition ptBottomPos; //< 下 + unsigned int nGray; +} SVzCircleMarkPosInfo; + +/// @brief +/// 3D圆形 +typedef struct +{ + float fR; + SVzNL3DPointF ptCenter; + SVzNL3DPointF ptWordCenter; + SVzNL3DPointF ptNormalVector; + SVzNL3DPointF* pArray3DPoint; + SVzNL3DPointF* pArrayWorld3DPoint; + int nPointCount; +} SVzNL3DCircleResult; + +/// @brief +/// 3D圆形物体 +typedef struct +{ + EVzNL3DCircleType eType; // 圆类型 + + SVzNLROIRect box2dLeft; // 包围盒左图 + SVzNLROIRect box2dRight; // 包围盒右图 + + SVzNL3DCircleResult* pArrayCircle; + int nCircleCount; +} SVzNL3DCircleObjectResult; + +/// @brief +/// 3D圆形 +typedef struct +{ + double dR; + SVzNL2DPoint ptLeftCenter; + SVzNL2DPoint ptRightCenter; + SVzNL2DPoint* pArray2DLeftPoint; + SVzNL2DPoint* pArray2DRightPoint; + int nPointCount; +} SVzNL2DCircleResult; + +/// @brief +/// 3D圆形物体 +typedef struct +{ + EVzNL3DCircleType eType; // 圆类型 + + SVzNLROIRect box2dLeft; // 包围盒左图 + SVzNLROIRect box2dRight; // 包围盒右图 + + SVzNL2DCircleResult* pArrayCircle; + int nCircleCount; +} SVzNL2DCircleObjectResult; + +/* + * @brief 眼睛配置信息 + */ +typedef struct +{ + EVzNLEthernetEyeIPType eEthernetIPType; + unsigned char byEyeIP[4]; + unsigned char byEyeMac[6]; + unsigned char byLocalIP[4]; + unsigned char byLocalMac[6]; + char vendor[8]; + char productName[8]; + char serialID[8]; + VzBool bIsSupportUDPFilter; + + SVzNetCardInfo sNetCardInfo; +} SVzNLEthernetEyeConfigInfo; + +typedef struct +{ + double dWhiteCheckPercent; + unsigned char chRgbMax[3]; + double dBlockEffectiveTh; + short shMinBlockNumber; + double dGainRange[2]; + unsigned char chHistStartAddr; + int nBlockWidth; + int nBlockHeight; +} SVzNLAWBInfo; + +// 钢筋检测模式 +enum +{ + keEyeCBDetectCounterMode_All = 1, + keEyeCBDetectCounterMode_Center = 2, +}; +typedef unsigned int EVzEyeCBDetectMode; + +// @brief 摆动机构信息 +struct SVzSwingMotorDevInfo +{ + int nMotorMaxAngle; //< 电机最大旋转角度 + int nMaxAngle; //< 最大旋转角度 + int nMinSpeed; //< 最小角速度 + int nMaxSpeed; //< 最大角速度 + int nStartAngle; //< 起始位置 + int nEndAngle; //< 结束位置 + int nSpeed; //< 速度 + int nStopAngle; //< 停止角度 + float fEndAngleThres; //< 结束角度判断阈值 +}; + +/// @brief +typedef struct +{ + unsigned int nAirforceCount; //< 气枪数 + unsigned int nStartAddress; //< 起始位置 + double dVolumeThres; //< 体积阈值 + unsigned int nMaxAirTimes; //< 最大喷气次数 + unsigned int nTimePerAir; //< 单次喷气时间 ms +} SVzDetectGangueParam; + +/// @brief +typedef struct +{ + double dArea; + SVzNL3DPoint ptPos[8]; +} SVzGangueObjectResult; + +/// @brief +/// Dump文件回调 +/// @param [out] szCrashFile Dump全路径 +typedef void(*VzNL_CrashCallBack)(char szCrashFile[VZ_MAXPATH]); + +/// @name 自动检测回调函数 +/// @{ +/** + * @brief 获取自动检测结果 + * @param [out] p3DPoint 3D数据 + * @param [out] p2DPoint 2D数据 + * @param [out] nCount 数据个数 + * @param [out] nTimeStamp 时间戳 + * @param [out] dTotleOffset 总偏移 + * @param [out] dStep 单次偏移 + * @param [out] pLeftImage 左图 + * @param [out] pRightImage 右图 + * @param [out] pParam 回调参数 + */ +typedef int(*VzNL_GetAutoDetectResultCB)(SVzNL3DPosition* p3DPoint, SVzNL2DPosition* p2DPoint, int nCount, unsigned long long nTimeStamp, double dTotleOffset, double dStep, SVzNLImageData* pLeftImage, SVzNLImageData* pRightImage, void* pParam); + +/// @brief 激光线数据 +typedef struct +{ + void* p3DPoint; //< 3D点 根据ResultDataType使用不同的数据结构 + void* p2DPoint; //< 2D点 根据ResultDataType使用不同的数据结构 + int nPointCount; //< 2D 3D 数据个数 + double dTotleOffset; //< 总偏移 + double dStep; //< 距离上一条线的偏移 + unsigned long long llFrameIdx; //< 帧号 + unsigned long long llTimeStamp; //< 时间戳 + unsigned int nEncodeNo; //< 编码器值 + float fSwingAngle; //< 当前摆动角度 + VzBool bEndOnceScan; //< 是否已完成一次扫描 +} SVzLaserLineData; + +typedef void(*VzNL_AutoOutputLaserLineExCB)(EVzResultDataType eDataType, SVzLaserLineData* pLaserLinePoint, void* pParam); + +typedef struct +{ + unsigned int nCount; + EVzResultDataType eDataType; + SVzLaserLineData* pLaserLineData; +}SVzLaserLineCacheData; + +/// @brief 激光线数据 +typedef struct +{ + SVzLaserLineData sLineData; //< + + SVzNLImageData* pImageData[3]; //< 图像数据 + void* pUnknownData; //< 未知数据 +} SVzLaserLineDataEx; + +/// @} + +/// @brief 检测码类型 +enum +{ + keLogisticsDetectCodeTypeNone = 0, //< 什么都不检测 + keLogisticsDetectCodeType_QRCode = 0x01, //< 二维码 + keLogisticsDetectCodeType_BarCode = 0x02, //< 条形码 +}; +typedef unsigned int EVzLogisticsDetectCodeType; + +/// @brief 扣图类型 +enum +{ + keLogisticsClipObjModeNone = 0, //< 不做抠图操作 + keLogisticsClipObjMode_Object = 0x01, //< 抠物体图 + keLogisticsClipObjMode_QRCode = 0x02, //< 抠二维码图 + keLogisticsClipObjMode_BarCode = 0x04, //< 抠条形码图 +}; +typedef unsigned int EVzLogisticsClipObjMode; + +// 物流触发位置 +enum +{ + keLogisticsPackageSyncTypeInvalid = 0, //< 不触发 + keLogisticsPackageSyncType_In, //< 物体进入时触发 + keLogisticsPackageSyncType_Out, //< 物体出来时触发 +}; +typedef unsigned int EVzLogisticsPackageSyncType; + +// 物流相机运行模式 +typedef enum +{ + keLogisticsRunMode_None = 0, //< 无 + keLogisticsRunMode_SoftSignal = 1, //< 软件使能开始检测 + keLogisticsRunMode_ExtSignal, //< 外部使能开始检测 + keLogisticsRunMode_AutoSignal, //< 自动检测 +} EVzLogisticsRunMode; + +// 物流结果状态 +typedef enum +{ + keLogisticsResultStatusInvalid = 0, //< 无效 + keLogisticsResultStatus_Begin, //< 物体开始 + keLogisticsResultStatus_Work, //< 物体运动中 返回点云数据 + keLogisticsResultStatus_End //< 当前物体结束 返回物体体积 +} EVzLogisticsResultStatus; + +// @brief 结果信息掩码 +enum +{ + keLogisticsResultMask_PointCloud = 0x01, //< 点云数据 + keLogisticsResultMask_ObjectInfo = 0x02, //< 物体结果数据 + keLogisticsResultMask_ColorImage = 0x04, //< 彩色图像 +}; +typedef unsigned int EVzLogisticsResultMask; + +// @brief 硬件使能线信号类型 +enum +{ + keEnLineInPoint_None = 0x00, //< 无效 + keEnLineInPoint_EnSignal = 0x01, //< 使能信号 + keEnLineInPoint_BiPhaseEncoder = 0x02, //< 双向编码器 +}; +typedef unsigned int EVzEnLineInPoint; + +// @brief +typedef struct +{ + char szCode[VZ_MAXPATH]; //< 码值 + VzBool bValidImage; + SVzNLImageData sCodeImageData; //< 码图像 + void* pVoid; +} SVzLogisticsCodeInfo; + +// @brief 物体结果描述 +typedef struct +{ + float fVolume; //< 体积 + float fLength; //< 长 + float fWidth; //< 宽 + float fHeight; //< 高 + + // ①------------② + // /| /| + //③------------④ | + //| | | | + //| | | | + //| | | | + //|⑤------------⑥ + // / / + //⑦------------⑧ + SVzNL3DPoint sConnerPoint[8]; //< 8角点 + SVzNL3DPoint* pOutlinePoint; //< 轮廓点 + unsigned int nOutlinePointCount; //< 轮廓点个数 + + SVzLogisticsCodeInfo* psQRCodeInfo; //< 二维码信息 + unsigned int nQRCodeCount; + SVzLogisticsCodeInfo* psBarCodeInfo; //< 条形码信息 + unsigned int nBarCodeCount; + + VzBool bValidObjImage; //< 物体图像有效 + SVzNLImageData sObjImageData; //< 物体图像 + void* pVoidObjData; +} SVzLogisticsObjInfo; + +// 物流结果信息 +typedef struct +{ + EVzLogisticsResultMask eMask; //< 结果标志 + + // @name PointClound + // @{ + unsigned long long ullTimeStamp; //< 时间戳 + unsigned long long ullEncodeNo; //< 编码器值 + SVzNL3DPosition* p3DPoint; //< 3D点 + unsigned int nPointCount; //< 点数 + double dOffset; //< 偏移 + // @} + + // @name ObjectInfo + // @{ + unsigned int nObjectCount; //< 物体个数 + SVzLogisticsObjInfo* pArrayObjectInfo; //< 物体数组 + + VzBool bIsResetOffset; //< 当前帧开始X值被重置 + // @} + + // @name ColorImage + // @{ + SVzNLImageData* psCenterImage; //< 图像 + unsigned int nCenterImageTimeStamp; //< 图像时间 + // @} +} SVzLogisticsResultData; + +typedef enum +{ + keLaserFileType_Invalid = 0, + keLaserFileType_PureTxt = 1, //< 纯文本 + keLaserFileType_Txt, //< 本公司自用 + keLaserFileType_Pcd, //< PCD + keLaserFileType_Las, //< LAS + keLaserFileType_VzBinaryData, //< Binary File + keLaserFileType_Ply, +} EVzLaserFileType; + +/// @brief 文件模式 +enum +{ + keFileModeInvalid = 0, + keFileModeRead = 0x01, + keFileModeWrite = 0x02, +}; +typedef unsigned int EVzFileMode; + +/// @brief 点云后处理图像类型定义 +typedef enum +{ + kePointAfterProcTypeInvalid = -1, + kePointAfterProcType_DepthMap, // 深度图 + kePointAfterProcType_GrayPic, // 灰度图 + kePointAfterProcType_Intensity, // 光强图 +} EVzPointAfterProcType; + +/* + * 机械臂点数据结构定义 + */ +typedef struct +{ + float x; + float y; + float z; + float roll; + float pitch; + float yaw; +} SVzNLRobotPointF; + +typedef struct +{ + SVzNLRobotPointF sPoint; // 坐标系内的点坐标 + SVzNL3DPointF sRX; // 坐标系 X方向 + SVzNL3DPointF sRY; // 坐标系 Y方向 + SVzNL3DPointF sRZ; // 坐标系 Z方向 +} SVzNLRobotPointWithPoseF; + +typedef struct +{ + SVzNL3DPointF sPoint; // 坐标系内的点坐标 + SVzNL3DPointF sRX; // 坐标系 X方向 + SVzNL3DPointF sRY; // 坐标系 Y方向 + SVzNL3DPointF sRZ; // 坐标系 Z方向 +} SVzNL3DPosePointF; + +// +typedef struct +{ + SVzNL3DPointF sPoint; // 点位置 + SVzNL3DPointF sNormal; // 法向量 +} SVzNL3DTracePointF; + +/// @brief 激光线数据 +typedef struct +{ + SVzLaserLineData sLineData; + + unsigned int nLeftPointCount; + unsigned int nRightPointCount; + + SVzNLPointXYZ* pLeftTearData; + SVzNLPointXYZ* pRightTearData; + unsigned int nLeftTearPointCount; + unsigned int nRightTearPointCount; +} SVzLaserTearDataEx; + +/// @brief +typedef enum +{ + keLaserObjectDetectPresetProfile_Custom = -1, + keLaserObjectDetectPresetProfile_General, //< 通用 + keLaserObjectDetectPresetProfile_ThinWire, //< 细铁丝 + keLaserObjectDetectPresetProfile_BigSence, //< 大景深 + keLaserObjectDetectPresetProfile_ReBar, //< 钢筋计数 + keLaserObjectDetectPresetProfileLast +} EVzLaserObjectDetectPresetProfile; + + +/// @brief +/// 激光线文件配置参数 +typedef enum +{ + keLaserFileParamStart = 0, + keLaserFileParam_Speed = keLaserFileParamStart, //< 速度值 类型 double 当前用于 binarydata 和 txt 两种文件存储 + keLaserFileParam_DataType, //< 数据类型 类型 unsigned int 当值为 0时表示Ascii模式,当为1时表示Binary模式,目前用于pcd的配置 + keLaserFileParam_SavePointCnt, //< 是否存储point个数 类型 unsigned int ,当值为0时表示不存储,为1表示存储,目前用于pure txt的配置 + keLaserFileParam_SaveSwingAngle, + keLaserFileParamLast +} EVzLaserFileParam; + +/// @brief +/// 触发输出模式 +typedef enum +{ + keStrobeTriggerOutMode_None = 0, //< 不控制触发输出 : 默认 + keStrobeTriggerOutMode_CameraLink, //< 跟随主模式输出 + keStrobeTriggerOutMode_UserControl, //< 用户控制 + keStrobeTriggerOutMode_LoopBack, //< 回环控制 + keStrobeTriggerOutMode_ServoControl,//< 舵机控制 +} EVzStrobeTriggerOutMode; + +/// @brief +/// Sensor 是否为长焦镜头 +enum EVzSensorLenType +{ + keSensorLenType_Invalid = 0, + keSensorLenType_3mm, + keSensorLenType_6mm, +}; + +/// @brief +/// 镜头盖住的状态类型 +enum EVzServoStatusType +{ + keServoStatusType_Invalid = -1, + keServoStatusType_Cover, + keServoStatusType_UnCover, +}; + +/// @brief +/// 编码器参数类型 +enum EVzEncodeParamType +{ + keEncodeParamType_RollerResolution = 0, //< 使用 编码器分辨率 + 编码器半径参数 模式 + keEncodeParamType_DistancePerPulse, //< 使用 编码器每两个脉冲的间距 +}; + +/// @brief +/// 配置多相机触发模式 +enum EVzMultiDevWorkMode +{ + keMultiDevWorkMode_MasterSlave = 0, ///< 主从触发 + keMultiDevWorkMode_Ext_FallingEdge = 1, ///< 外部下降沿触发 + keMultiDevWorkMode_Ext_HighFrequenceFallingEdge = 2, ///< 外部高帧率下降沿触发 +}; + +/// @brief +/// 配置融合的相机类型 +enum EVzMultiDevType +{ + keMultiDev_Dynanmic = 0, ///< 动态相机融合 + keMultiDev_Static = 1, ///< 静态相机融合 +}; + +/// @brief +/// 配置静态相机融合扫描模式 +enum EVzMultiStaticDevScanMode +{ + keMultiStaticDevScanMode_OneByOne = 0, ///< 一个接一个的扫描 + keMultiStaticDevScanMode_SameTime = 1, ///< 同时扫描 +}; + +/// @brief +/// 编码器方向 +enum EVzEncoderRollingDirection +{ + keEncoderRollingDirection_Forward = 0, + keEncoderRollingDirection_Backward +}; + + +/// @brief +/// 旋转图像类型 +enum EVzImageRotateType +{ + keImageRotateType_Invalid = -1, + keImageRotateType_FlipY_Rotate = 0, + keImageRotateType_ClockWise = 1, + keImageRotateType_CounterClockWise = 2, + keImageRotateType_90ClockWise = 3, + keImageRotateType_FlipX = 4, +}; + +typedef struct +{ + unsigned int nEncodeNo[2]; + EVzEncoderRollingDirection eDirection; + unsigned int nFrequency[2]; +} SVzEncoderData; + +enum +{ + keEmulationDetectType_AutoDetect = 0, + keEmulationDetectType_Detect, +}; +using EVzEmulationDetectType = unsigned int; + +/// @brief +/// 旋转图像类型 +enum EVzDustCoverConfigType +{ + keDustCoverConfigType_Large = 0, + keDustCoverConfigType_Medium, + keDustCoverConfigType_Small, +}; + +/// @brief +//环境类型 +typedef enum +{ + keEnvType_Invalid = -1, + keEnvType_InDoor = 0, + keEnvType_OutDoor = 1, +} EVzEnvType; + +/// @brief 范围类型 int +typedef struct +{ + EVzEnvType eEnvType; + unsigned int nLevel; +} SVzEnvLevel; + +/// @brief +//抗反光增益 +typedef enum +{ + keAntiReflectGainType_Close = 1, + keAntiReflectGainType_Double = 2, + keAntiReflectGainType_Quadruple = 4, + keAntiReflectGainType_Sextuple = 6, + keAntiReflectGainType_Octuple = 8, +} EVzAntiReflectGainType; + +typedef enum +{ + keAntiReflectCmpsDirType_LeftToRight = 0, + keAntiReflectCmpsDirType_RightToLeft = 1, +} EVzAntiReflectCmpsDirType; + +typedef struct +{ + unsigned int m_nCheckTimeInterval; + unsigned int m_nCheckTimeOut; + unsigned int m_nCheckExposeRatio; +}SVzAutoCoverControlParam; + +typedef struct +{ + char chIP[VZNL_SDK_NETWORK_IPv4_LENGTH]; +}SVzNLDeviceIp; + +typedef enum +{ + keLRSensorType_Gray = 0, + keLRSensorType_Color = 1, +} EVzLRSensorType; + +typedef enum +{ + keRGBOutputChannelType_R = 0, + keRGBOutputChannelType_G = 1, + keRGBOutputChannelType_B = 2, + keRGBOutputChannelType_All = 3, +} EVzRGBOutputChannelType; + +/// @brief +// 多相机融合时各子设备融合配置参数 +typedef struct +{ + unsigned int nDevIdx; // 融合索引:0:主设备;>0:从设备; + double dFusionMatrix[3][4]; // 融合矩阵 + double dYRange[2]; // 当前设备融合时Y轴过滤范围 +}SVzNLDeviceFusionData; + +// RGBHDR 出图模式 +typedef enum +{ + keRGBHDROutImageMode_Ori = 0, // 输出原始图像:背景图 + 熔池图 + keRGBHDROutImageMode_HDR = 1, // 输出HDR 叠加后的图像:HDR图 + keRGBHDROutImageMode_MPool = 2, // 只输出熔池图像 + keRGBHDROutImageMode_BG = 3, // 只输出背景图 + keRGBHDROutImageMode_Num // 模式个数 +} EVzRGBHDROutImageMode; + +/** +* @brief 点云<->2D伪彩图转换参数结构体 +*/ +typedef struct +{ + unsigned int nEdgeMargin; + double d3DXMin; + double d3DXMax; + double d3DYMin; + double d3DYMax; + double dPixelResolution; +}SVzPColorImageConvertInfo; + +/** +* @brief 2D伪彩图坐标<->3D坐标选取方法枚举 +*/ +typedef enum +{ + kePColorImageTo3DPoint_NearestPoint = 0, // 输出距离回算点X/Y 最近的原始点云3D 点坐标。 + kePColorImageTo3DPoint_RealPoint // 输出实际回算点X/Y坐标 + 最邻近原始点的Z坐标。 +}SVzPColorImageTo3DPointMethod; + +/// @brief +/// 采集图像模式 +typedef enum +{ + keCaptureMode_LR_Image = 0, // 输出双目图像 + keCaptureMode_C_Image, // 输出RGB图像 + keCaptureMode_LRC_Image, // 输出双目+RGB图像 +}EVzCaptureMode; + +/// @brief 焊缝跟踪点云扫描配置参数(存储到相机内) +typedef struct +{ + // 可配参数部分 + bool bIsEnableCalibROI; // 是否启用校正图 + EVzNLExposeMode eExposeMode; // 曝光模式 + unsigned int nExposeTime; // 曝光值 + unsigned short nLeftCameraGain; // 左目增益 + unsigned short nRightCameraGain; // 右目增益 + unsigned short nDynamicScanAngle; // 动态扫描时摆动机构角度 + unsigned short nFltrRgnTh; // 小信号过滤值 + SVzNLROIRect sLeftROI; // 左目ROI + SVzNLROIRect sRightROI; // 右目ROI + unsigned int nFrameRate; // 采集帧率 + unsigned int nOutputFrameRate; // 输出帧率:对接收到的激光线进一步抽样输出(0:不进行抽帧处理) + // 固定参数部分(SDK 内部自动设置),无需修改 + bool bIsSwingEnable; // 是否使能摆动机构,固定为false + bool bIsRGBEnable; // 是否使能RGB,固定为false + bool bIsStrobeEnable; // 是否使能C版功能,固定为true + bool bIsAntiReflectGainEnable; // 是否使能抗强光增益,固定为true +} SVzTrackingScanPara; + + +typedef struct{ + int denoiseStrength; // 去噪强度 + int sharpenStrength; // 锐化强度 + int brightness; // 亮度 + int contrast; // 对比度 + bool useAdvancedDenoise; // 使用高级去噪 +}AlgorithmParams; + +/** +* @brief 多相机融合操作,重叠区数据处理方法 +*/ +typedef enum +{ + keMulDevOverlayProcessType_SeqFilter = 0, // 按Y轴顺序过滤重叠的点云 + keMulDevOverlayProcessType_PointsFilter, // 按重叠区域内点数据个数过滤数据少的相机的点云 + keMulDevOverlayProcessType_DataFusion, // 对重叠区域内的点数据进行数据融合,补齐缺失的数据 +}EVzMulDevOverlayProcessType; + + +// Feature标志位定义 +typedef enum +{ + KE_FEATURE_RGB_CAMERA = 0x01, // RGB相机 + KE_FEATURE_SWING_MECHANISM = 0x02, // 摆动机构 + KE_FEATURE_DUAL_OUTPUT = 0x04, // 双端出图 + KE_FEATURE_STEREO_COLOR = 0x08, // 双目彩色 + KE_FEATURE_WELDING_CAMERA = 0x10 // 焊接相机 +}EVzFeatureFlags; + +// Feature数据结构 +typedef struct +{ + unsigned short flags; // 特性标志位 + unsigned short pupilDistance; // 相机瞳距配置 +}SVzFeatureData; + +/* +* 命令操作类型定义 +*/ +enum EVzUSBSerialRelayCmdType +{ + keUSBSerialRelayCmdType_CloseNoAck = 0, // 关闭不反馈 + keUSBSerialRelayCmdType_OpenNoAck, // 打开不反馈 + keUSBSerialRelayCmdType_CloseWithAck, // 关闭并反馈 + keUSBSerialRelayCmdType_OpenWithACck, // 打开并反馈 + keUSBSerialRelayCmdType_ReverseWithAck, // 取反并反馈 + keUSBSerialRelayCmdType_CheckState, // 查询状态 + keUSBSerialRelayCmdType_FlashBreakWithAck, // 闪断并反馈 + keUSBSerialRelayCmdType_Num +}; + +/* +* 串口波特率枚举 +*/ +enum EVzSerialPortBaudRate +{ + keSerialPortBaudRate_1200 = 0, + keSerialPortBaudRate_2400, + keSerialPortBaudRate_4800, + keSerialPortBaudRate_9600, + keSerialPortBaudRate_19200, + keSerialPortBaudRate_38400, + keSerialPortBaudRate_57600, + keSerialPortBaudRate_115200, + keSerialPortBaudRate_Num +}; + +/* +* 串口数据长度枚举(4~8bits) +*/ +enum EVzSerialPortByteSize +{ + keSerialPortByteSize_4 = 0, + keSerialPortByteSize_5, + keSerialPortByteSize_6, + keSerialPortByteSize_7, + keSerialPortByteSize_8, + keSerialPortByteSize_Num +}; + +/* +* 串口数据校验位枚举(0-4=None,Odd,Even,Mark,Space) +*/ +enum EVzSerialPortParity +{ + keSerialPortParity_None = 0, + keSerialPortParity_Odd, + keSerialPortParity_Even, + keSerialPortParity_Mark, + keSerialPortParity_Space, + keSerialPortParity_Num +}; + +/* +* 串口数据停止位枚举(0,1,2 = 1, 1.5, 2) +*/ +enum EVzSerialPortStopBits +{ + keSerialPortStopBits_1 = 0, + keSerialPortStopBits_1_5, + keSerialPortStopBits_2, + keSerialPortStopBits_Num +}; + +/* +* 串口配置参数结构 +*/ +typedef struct +{ + EVzSerialPortBaudRate eBaudRate; // 串口波特率 + EVzSerialPortByteSize eByteSize; // 串口波特率 + EVzSerialPortParity eParity; // 串口校验位 + EVzSerialPortStopBits eStopBits; // 串口停止位 +}SVzSerialPortCfgPara; + +/* +* USB 继电器设备参数 +*/ +typedef struct +{ + unsigned int nChannelNum; // 模块控制的通道个数 + SVzSerialPortCfgPara sSerialPortCfgPara; // 串口控制参数 +}SVzUSBSerialRelayCfgPara; + +/* +* 搜索输出的信息结构,目前只包括设备端口名称 +*/ +typedef struct +{ + char sPortName[VZNL_DEVICE_NAME_LENGTH]; +}SVzSerialPortInfo; + +/* +* +*/ +typedef struct +{ + char szName[VZ_USERCONFIG_NAME_LENGTH]; + char szDesc[VZ_USERCONFIG_DESC_LENGTH]; +}SVzUserConfigInfo; + + +#endif //__VIZUM_TYPES_HEADER_