From ce4bd9348a74e7f91d471a4c9a8afcacbecf4984 Mon Sep 17 00:00:00 2001 From: yiyi Date: Sun, 22 Feb 2026 00:10:46 +0800 Subject: [PATCH] =?UTF-8?q?=E6=89=8B=E7=9C=BC=E6=A0=87=E5=AE=9A=E5=AE=8C?= =?UTF-8?q?=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- GrabBagPrj/CalibView.iss | 46 + GrabBagPrj/pkg_calibview.bat | 34 + GrabBagPrj/pkg_cloudview.bat | 2 +- Tools/CalibView/Inc/CalibDataWidget.h | 415 ++--- Tools/CalibView/Inc/CalibViewMainWindow.h | 334 ++-- Tools/CalibView/Src/CalibDataWidget.cpp | 1564 ++++++++++--------- Tools/CalibView/Src/CalibViewMainWindow.cpp | 1417 +++++++++-------- Utils | 2 +- 8 files changed, 2073 insertions(+), 1741 deletions(-) create mode 100644 GrabBagPrj/CalibView.iss create mode 100644 GrabBagPrj/pkg_calibview.bat diff --git a/GrabBagPrj/CalibView.iss b/GrabBagPrj/CalibView.iss new file mode 100644 index 0000000..adc5316 --- /dev/null +++ b/GrabBagPrj/CalibView.iss @@ -0,0 +1,46 @@ +; Script generated by the Inno Setup Script Wizard. +; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! + +#define MyAppName "手眼标定工具" +#define MyAppVersion "1.0.0" +#define MyAppPublisher "" +#define MyAppURL "" +#define MyAppExeName "CalibView.exe" +#define MyProgramPath ".\buildwin\CalibView" + +[Setup] +; NOTE: The value of AppId uniquely identifies this application. +; Do not use the same AppId value in installers for other applications. +; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) +AppId={{F8A7B6C5-D4E3-4F2A-9B1C-0D8E7F6A5B4C}} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +AppVerName={#MyAppName}_V{#MyAppVersion} +AppPublisher={#MyAppPublisher} +AppPublisherURL={#MyAppURL} +AppSupportURL={#MyAppURL} +AppUpdatesURL={#MyAppURL} +DefaultDirName={pf}\{#MyAppName} +DisableProgramGroupPage=yes +OutputBaseFilename={#MyAppName}V{#MyAppVersion} +OutputDir=..\Publish +Compression=lzma +SolidCompression=yes + +[Languages] +Name: "default"; MessagesFile: "compiler:Languages\ChineseSimplified.isl" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked + +[Files] +Source: "{#MyProgramPath}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs + +[Icons] +Name: "{commonprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" +Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon + +[Run] +Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent runascurrentuser + +[Code] diff --git a/GrabBagPrj/pkg_calibview.bat b/GrabBagPrj/pkg_calibview.bat new file mode 100644 index 0000000..d4a93ed --- /dev/null +++ b/GrabBagPrj/pkg_calibview.bat @@ -0,0 +1,34 @@ +@echo off +setlocal + +set PRJ_PATH=c:\project\QT\GrabBag\GrabBagPrj + +if not exist "%PRJ_PATH%\buildwin\CalibView" mkdir "%PRJ_PATH%\buildwin\CalibView" + +cd /d "%PRJ_PATH%\buildwin\CalibView" + +powershell -Command "Remove-Item * -Recurse -Force" + +copy "..\..\build\Tools\CalibView\release\CalibView.exe" .\ + +:: VzNLSDK DLL +copy "..\..\SDK\Device\VzNLSDK\Windows\x64\Release\VzKernel.dll" .\ +copy "..\..\SDK\Device\VzNLSDK\Windows\x64\Release\VzNLDetect.dll" .\ +copy "..\..\SDK\Device\VzNLSDK\Windows\x64\Release\VzNLGraphics.dll" .\ +copy "..\..\SDK\Device\VzNLSDK\Windows\x64\Release\VzLog.dll" .\ +copy "..\..\SDK\Device\VzNLSDK\Windows\x64\Release\libViEyeApi.dll" .\ + +:: OpenCV DLL +copy "..\..\SDK\OpenCV320\Windows\vc14\Release\opencv_world320.dll" .\ + +"C:\tools\Qt\5.15.2\msvc2019_64\bin\windeployqt.exe" "CalibView.exe" + +cd ..\.. + +set ISCC_PATH=C:\Program Files (x86)\Inno Setup 6\ISCC.exe + +"%ISCC_PATH%" "CalibView.iss" + +echo "finish" + +endlocal diff --git a/GrabBagPrj/pkg_cloudview.bat b/GrabBagPrj/pkg_cloudview.bat index 9c5bab1..a722888 100644 --- a/GrabBagPrj/pkg_cloudview.bat +++ b/GrabBagPrj/pkg_cloudview.bat @@ -9,7 +9,7 @@ cd /d "%PRJ_PATH%\buildwin\CloudView" powershell -Command "Remove-Item * -Recurse -Force" -copy "..\..\build\Tools\CloudView\release\CloudView.exe" .\ +copy "..\..\build\Utils\CloudView\release\CloudView.exe" .\ "C:\tools\Qt\5.15.2\msvc2019_64\bin\windeployqt.exe" "CloudView.exe" diff --git a/Tools/CalibView/Inc/CalibDataWidget.h b/Tools/CalibView/Inc/CalibDataWidget.h index 8167f96..921c755 100644 --- a/Tools/CalibView/Inc/CalibDataWidget.h +++ b/Tools/CalibView/Inc/CalibDataWidget.h @@ -1,200 +1,215 @@ -#ifndef CALIBDATAWIDGET_H -#define CALIBDATAWIDGET_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "HandEyeCalibTypes.h" - -/** - * @brief 标定数据输入控件 - * 支持 Eye-To-Hand、Eye-In-Hand 和 TCP 三种模式的数据输入 - */ -class CalibDataWidget : public QWidget -{ - Q_OBJECT - -public: - explicit CalibDataWidget(QWidget* parent = nullptr); - ~CalibDataWidget() override; - - /** - * @brief 获取 Eye-To-Hand 标定数据 - */ - void getEyeToHandData(std::vector& eyePoints, - std::vector& robotPoints) const; - - /** - * @brief 获取 Eye-In-Hand 标定数据 - */ - void getEyeInHandData(std::vector& calibData) const; - - /** - * @brief 获取当前标定模式 - */ - HECCalibrationType getCalibType() const; - - /** - * @brief 清除所有数据 - */ - void clearAll(); - - /** - * @brief 供外部设置机械臂数据到输入框(根据当前模式自动填充) - */ - void setRobotInput(double x, double y, double z, double rx, double ry, double rz); - - /** - * @brief 供外部设置相机数据到输入框(Eye-To-Hand 模式) - */ - void setCameraInput(double x, double y, double z, double rx, double ry, double rz); - - /** - * @brief 获取 TCP 标定输入数据 - */ - HECTCPCalibData getTCPCalibData() const; - -signals: - /** - * @brief 标定模式改变信号 - */ - void calibTypeChanged(HECCalibrationType type); - - /** - * @brief 请求 Eye-To-Hand 标定 - */ - void requestEyeToHandCalib(); - - /** - * @brief 请求 Eye-In-Hand 标定 - */ - void requestEyeInHandCalib(); - - /** - * @brief 请求 TCP 标定 - */ - void requestTCPCalib(); - - -private slots: - /** - * @brief 标定模式切换 - */ - void onCalibTypeChanged(int index); - - /** - * @brief TCP 模式切换(3-DOF / 6-DOF) - */ - void onTCPModeChanged(int index); - - /** - * @brief TCP 添加行 - */ - void onTCPAddRow(); - - /** - * @brief TCP 删除行 - */ - void onTCPRemoveRow(); - -private: - /** - * @brief 初始化界面 - */ - void setupUI(); - - /** - * @brief 创建 Eye-To-Hand 数据表格 - */ - QWidget* createEyeToHandGroup(); - - /** - * @brief 创建 Eye-In-Hand 数据表格 - */ - QWidget* createEyeInHandGroup(); - - /** - * @brief 创建 TCP 标定数据组 - */ - QWidget* createTCPCalibGroup(); - - - /** - * @brief 更新表格显示 - */ - void updateTableVisibility(); - - // 标定模式选择 - QComboBox* m_cbCalibType; - - // Eye-To-Hand 数据表格 - QTableWidget* m_tableEyeToHand; - QWidget* m_groupEyeToHand; - - // Eye-In-Hand 数据表格 - QTableWidget* m_tableEyeInHand; - QWidget* m_groupEyeInHand; - - // Eye-To-Hand 内联按钮 - QPushButton* m_btnEyeToHandAddRow; - QPushButton* m_btnEyeToHandDeleteRow; - QPushButton* m_btnEyeToHandCalib; - - // Eye-In-Hand 内联按钮 - QPushButton* m_btnEyeInHandAddRow; - QPushButton* m_btnEyeInHandDeleteRow; - QPushButton* m_btnEyeInHandCalib; - - // TCP 标定相关 - QWidget* m_groupTCPCalib; - QTableWidget* m_tableTCP; - QComboBox* m_tcpModeCombo; - QComboBox* m_tcpEulerOrderCombo; - QSpinBox* m_tcpRefPoseIndex; - QDoubleSpinBox* m_tcpWorldRx; - QDoubleSpinBox* m_tcpWorldRy; - QDoubleSpinBox* m_tcpWorldRz; - QGroupBox* m_tcpOrientationGroup; - QPushButton* m_tcpAddRowBtn; - QPushButton* m_tcpRemoveRowBtn; - QPushButton* m_btnTCPCalib; - - // Eye-To-Hand 输入框 - QDoubleSpinBox* m_inputEyeX; - QDoubleSpinBox* m_inputEyeY; - QDoubleSpinBox* m_inputEyeZ; - QDoubleSpinBox* m_inputRobotX; - QDoubleSpinBox* m_inputRobotY; - QDoubleSpinBox* m_inputRobotZ; - QPushButton* m_btnEyeToHandAddInput; - - // Eye-In-Hand 输入框 - QDoubleSpinBox* m_inputEndX; - QDoubleSpinBox* m_inputEndY; - QDoubleSpinBox* m_inputEndZ; - QDoubleSpinBox* m_inputEndRoll; - QDoubleSpinBox* m_inputEndPitch; - QDoubleSpinBox* m_inputEndYaw; - QDoubleSpinBox* m_inputCamX; - QDoubleSpinBox* m_inputCamY; - QDoubleSpinBox* m_inputCamZ; - QPushButton* m_btnEyeInHandAddInput; - - // TCP 输入框 - QDoubleSpinBox* m_inputTcpX; - QDoubleSpinBox* m_inputTcpY; - QDoubleSpinBox* m_inputTcpZ; - QDoubleSpinBox* m_inputTcpRx; - QDoubleSpinBox* m_inputTcpRy; - QDoubleSpinBox* m_inputTcpRz; - QPushButton* m_btnTcpAddInput; -}; - -#endif // CALIBDATAWIDGET_H +#ifndef CALIBDATAWIDGET_H +#define CALIBDATAWIDGET_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "HandEyeCalibTypes.h" + +/** + * @brief 标定数据输入控件 + * 支持 Eye-To-Hand、Eye-In-Hand 和 TCP 三种模式的数据输入 + */ +class CalibDataWidget : public QWidget +{ + Q_OBJECT + +public: + explicit CalibDataWidget(QWidget* parent = nullptr); + ~CalibDataWidget() override; + + /** + * @brief 获取 Eye-To-Hand 标定数据 + */ + void getEyeToHandData(std::vector& eyePoints, + std::vector& robotPoints) const; + + /** + * @brief 获取 Eye-In-Hand 标定数据 + */ + void getEyeInHandData(std::vector& calibData) const; + + /** + * @brief 获取当前标定模式 + */ + HECCalibrationType getCalibType() const; + + /** + * @brief 获取当前标定模式索引 (0=EyeToHand, 1=EyeInHand, 2=TCP) + */ + int getCalibTypeIndex() const; + + /** + * @brief 清除所有数据 + */ + void clearAll(); + + /** + * @brief 供外部设置机械臂数据到输入框(根据当前模式自动填充) + */ + void setRobotInput(double x, double y, double z, double rx, double ry, double rz); + + /** + * @brief 供外部设置相机数据到输入框(Eye-To-Hand 模式) + */ + void setCameraInput(double x, double y, double z, double rx, double ry, double rz); + + /** + * @brief 获取 TCP 标定输入数据 + */ + HECTCPCalibData getTCPCalibData() const; + + /** + * @brief 将当前模式的标定数据保存到 INI 文件 + */ + void saveCalibData(const QString& filePath) const; + + /** + * @brief 从 INI 文件加载标定数据到表格 + */ + void loadCalibData(const QString& filePath); + +signals: + /** + * @brief 标定模式改变信号 + */ + void calibTypeChanged(HECCalibrationType type); + + /** + * @brief 请求 Eye-To-Hand 标定 + */ + void requestEyeToHandCalib(); + + /** + * @brief 请求 Eye-In-Hand 标定 + */ + void requestEyeInHandCalib(); + + /** + * @brief 请求 TCP 标定 + */ + void requestTCPCalib(); + + +private slots: + /** + * @brief 标定模式切换 + */ + void onCalibTypeChanged(int index); + + /** + * @brief TCP 模式切换(3-DOF / 6-DOF) + */ + void onTCPModeChanged(int index); + + /** + * @brief TCP 添加行 + */ + void onTCPAddRow(); + + /** + * @brief TCP 删除行 + */ + void onTCPRemoveRow(); + +private: + /** + * @brief 初始化界面 + */ + void setupUI(); + + /** + * @brief 创建 Eye-To-Hand 数据表格 + */ + QWidget* createEyeToHandGroup(); + + /** + * @brief 创建 Eye-In-Hand 数据表格 + */ + QWidget* createEyeInHandGroup(); + + /** + * @brief 创建 TCP 标定数据组 + */ + QWidget* createTCPCalibGroup(); + + + /** + * @brief 更新表格显示 + */ + void updateTableVisibility(); + + // 标定模式选择 + QComboBox* m_cbCalibType; + + // Eye-To-Hand 数据表格 + QTableWidget* m_tableEyeToHand; + QWidget* m_groupEyeToHand; + + // Eye-In-Hand 数据表格 + QTableWidget* m_tableEyeInHand; + QWidget* m_groupEyeInHand; + + // Eye-To-Hand 内联按钮 + QPushButton* m_btnEyeToHandAddRow; + QPushButton* m_btnEyeToHandDeleteRow; + QPushButton* m_btnEyeToHandCalib; + + // Eye-In-Hand 内联按钮 + QPushButton* m_btnEyeInHandAddRow; + QPushButton* m_btnEyeInHandDeleteRow; + QPushButton* m_btnEyeInHandCalib; + + // TCP 标定相关 + QWidget* m_groupTCPCalib; + QTableWidget* m_tableTCP; + QComboBox* m_tcpModeCombo; + QComboBox* m_tcpEulerOrderCombo; + QSpinBox* m_tcpRefPoseIndex; + QDoubleSpinBox* m_tcpWorldRx; + QDoubleSpinBox* m_tcpWorldRy; + QDoubleSpinBox* m_tcpWorldRz; + QGroupBox* m_tcpOrientationGroup; + QPushButton* m_tcpAddRowBtn; + QPushButton* m_tcpRemoveRowBtn; + QPushButton* m_btnTCPCalib; + + // Eye-To-Hand 输入框 + QDoubleSpinBox* m_inputEyeX; + QDoubleSpinBox* m_inputEyeY; + QDoubleSpinBox* m_inputEyeZ; + QDoubleSpinBox* m_inputRobotX; + QDoubleSpinBox* m_inputRobotY; + QDoubleSpinBox* m_inputRobotZ; + QPushButton* m_btnEyeToHandAddInput; + + // Eye-In-Hand 输入框 + QDoubleSpinBox* m_inputEndX; + QDoubleSpinBox* m_inputEndY; + QDoubleSpinBox* m_inputEndZ; + QDoubleSpinBox* m_inputEndRoll; + QDoubleSpinBox* m_inputEndPitch; + QDoubleSpinBox* m_inputEndYaw; + QDoubleSpinBox* m_inputCamX; + QDoubleSpinBox* m_inputCamY; + QDoubleSpinBox* m_inputCamZ; + QPushButton* m_btnEyeInHandAddInput; + + // TCP 输入框 + QDoubleSpinBox* m_inputTcpX; + QDoubleSpinBox* m_inputTcpY; + QDoubleSpinBox* m_inputTcpZ; + QDoubleSpinBox* m_inputTcpRx; + QDoubleSpinBox* m_inputTcpRy; + QDoubleSpinBox* m_inputTcpRz; + QPushButton* m_btnTcpAddInput; +}; + +#endif // CALIBDATAWIDGET_H diff --git a/Tools/CalibView/Inc/CalibViewMainWindow.h b/Tools/CalibView/Inc/CalibViewMainWindow.h index f832dfd..6f65cbd 100644 --- a/Tools/CalibView/Inc/CalibViewMainWindow.h +++ b/Tools/CalibView/Inc/CalibViewMainWindow.h @@ -1,162 +1,172 @@ -#ifndef CALIBVIEWMAINWINDOW_H -#define CALIBVIEWMAINWINDOW_H - -#include -#include -#include -#include -#include -#include -#include - -#include "IHandEyeCalib.h" - -class CalibDataWidget; -class CalibResultWidget; -class MainWindow; // RobotView::MainWindow -class VrEyeViewWidget; - -/** - * @brief 手眼标定测试工具主窗口 - * 用于测试 HandEyeCalib 模块的各项功能 - */ -class CalibViewMainWindow : public QMainWindow -{ - Q_OBJECT - -public: - explicit CalibViewMainWindow(QWidget* parent = nullptr); - ~CalibViewMainWindow() override; - -signals: - /** - * @brief 标定完成信号 - */ - void calibrationCompleted(const HECCalibResult& result); - -private slots: - /** - * @brief 执行 Eye-To-Hand 标定 - */ - void onEyeToHandCalib(); - - /** - * @brief 执行 Eye-In-Hand 标定 - */ - void onEyeInHandCalib(); - - /** - * @brief 执行坐标变换测试 - */ - void onTransformTest(); - - /** - * @brief 执行欧拉角转换测试 - */ - void onEulerTest(); - - /** - * @brief 执行 TCP 标定 - */ - void onTCPCalib(); - - /** - * @brief 清除所有数据 - */ - void onClearAll(); - - /** - * @brief 保存标定结果 - */ - void onSaveResult(); - - /** - * @brief 加载标定结果 - */ - void onLoadResult(); - - /** - * @brief 打开 RobotView 窗口 - */ - void onOpenRobotView(); - - /** - * @brief 接收 RobotView 的 TCP 位姿 - */ - void onRobotTcpPoseReceived(double x, double y, double z, - double rx, double ry, double rz); - - /** - * @brief 打开 VrEyeView 窗口 - */ - void onOpenVrEyeView(); - - /** - * @brief 接收 VrEyeView 的标定板检测结果 - */ - void onChessboardDetected(double x, double y, double z, - double rx, double ry, double rz); - -private: - /** - * @brief 初始化界面 - */ - void setupUI(); - - /** - * @brief 创建菜单栏 - */ - void createMenuBar(); - - /** - * @brief 创建右侧面板 - */ - QWidget* createRightPanel(); - - /** - * @brief 更新状态栏 - */ - void updateStatusBar(const QString& message); - - /** - * @brief 追加日志 - */ - void appendLog(const QString& message); - - // 标定实例 - IHandEyeCalib* m_calib; - - // 数据输入控件 - CalibDataWidget* m_dataWidget; - - // 结果显示控件 - CalibResultWidget* m_resultWidget; - - // 右侧面板 - 坐标变换测试 - QDoubleSpinBox* m_sbTransformX; - QDoubleSpinBox* m_sbTransformY; - QDoubleSpinBox* m_sbTransformZ; - QPushButton* m_btnTransform; - - // 右侧面板 - 欧拉角转换测试 - QDoubleSpinBox* m_sbRoll; - QDoubleSpinBox* m_sbPitch; - QDoubleSpinBox* m_sbYaw; - QComboBox* m_cbEulerOrder; - QPushButton* m_btnEulerConvert; - - // 右侧面板 - 日志 - QTextEdit* m_logEdit; - - // 当前标定结果 - HECCalibResult m_currentResult; - bool m_hasResult; - - // RobotView 窗口实例 - MainWindow* m_robotView; - - // VrEyeView 窗口实例 - VrEyeViewWidget* m_vrEyeView; -}; - -#endif // CALIBVIEWMAINWINDOW_H +#ifndef CALIBVIEWMAINWINDOW_H +#define CALIBVIEWMAINWINDOW_H + +#include +#include +#include +#include +#include +#include +#include + +#include "IHandEyeCalib.h" + +class CalibDataWidget; +class CalibResultWidget; +class MainWindow; // RobotView::MainWindow +class VrEyeViewWidget; + +/** + * @brief 手眼标定测试工具主窗口 + * 用于测试 HandEyeCalib 模块的各项功能 + */ +class CalibViewMainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit CalibViewMainWindow(QWidget* parent = nullptr); + ~CalibViewMainWindow() override; + +signals: + /** + * @brief 标定完成信号 + */ + void calibrationCompleted(const HECCalibResult& result); + +private slots: + /** + * @brief 执行 Eye-To-Hand 标定 + */ + void onEyeToHandCalib(); + + /** + * @brief 执行 Eye-In-Hand 标定 + */ + void onEyeInHandCalib(); + + /** + * @brief 执行坐标变换测试 + */ + void onTransformTest(); + + /** + * @brief 执行欧拉角转换测试 + */ + void onEulerTest(); + + /** + * @brief 执行 TCP 标定 + */ + void onTCPCalib(); + + /** + * @brief 清除所有数据 + */ + void onClearAll(); + + /** + * @brief 保存标定结果 + */ + void onSaveResult(); + + /** + * @brief 加载标定结果 + */ + void onLoadResult(); + + /** + * @brief 保存标定数据(表格中的坐标数据) + */ + void onSaveCalibData(); + + /** + * @brief 加载标定数据(表格中的坐标数据) + */ + void onLoadCalibData(); + + /** + * @brief 打开 RobotView 窗口 + */ + void onOpenRobotView(); + + /** + * @brief 接收 RobotView 的 TCP 位姿 + */ + void onRobotTcpPoseReceived(double x, double y, double z, + double rx, double ry, double rz); + + /** + * @brief 打开 VrEyeView 窗口 + */ + void onOpenVrEyeView(); + + /** + * @brief 接收 VrEyeView 的标定板检测结果 + */ + void onChessboardDetected(double x, double y, double z, + double rx, double ry, double rz); + +private: + /** + * @brief 初始化界面 + */ + void setupUI(); + + /** + * @brief 创建菜单栏 + */ + void createMenuBar(); + + /** + * @brief 创建右侧面板 + */ + QWidget* createRightPanel(); + + /** + * @brief 更新状态栏 + */ + void updateStatusBar(const QString& message); + + /** + * @brief 追加日志 + */ + void appendLog(const QString& message); + + // 标定实例 + IHandEyeCalib* m_calib; + + // 数据输入控件 + CalibDataWidget* m_dataWidget; + + // 结果显示控件 + CalibResultWidget* m_resultWidget; + + // 右侧面板 - 坐标变换测试 + QDoubleSpinBox* m_sbTransformX; + QDoubleSpinBox* m_sbTransformY; + QDoubleSpinBox* m_sbTransformZ; + QPushButton* m_btnTransform; + + // 右侧面板 - 欧拉角转换测试 + QDoubleSpinBox* m_sbRoll; + QDoubleSpinBox* m_sbPitch; + QDoubleSpinBox* m_sbYaw; + QComboBox* m_cbEulerOrder; + QPushButton* m_btnEulerConvert; + + // 右侧面板 - 日志 + QTextEdit* m_logEdit; + + // 当前标定结果 + HECCalibResult m_currentResult; + bool m_hasResult; + + // RobotView 窗口实例 + MainWindow* m_robotView; + + // VrEyeView 窗口实例 + VrEyeViewWidget* m_vrEyeView; +}; + +#endif // CALIBVIEWMAINWINDOW_H diff --git a/Tools/CalibView/Src/CalibDataWidget.cpp b/Tools/CalibView/Src/CalibDataWidget.cpp index 3eb9c37..6f457f2 100644 --- a/Tools/CalibView/Src/CalibDataWidget.cpp +++ b/Tools/CalibView/Src/CalibDataWidget.cpp @@ -1,701 +1,863 @@ -#include "CalibDataWidget.h" -#include "../../SpinBoxPasteHelper.h" - -#include -#include -#include -#include -#include - -CalibDataWidget::CalibDataWidget(QWidget* parent) - : QWidget(parent) - , m_cbCalibType(nullptr) - , m_tableEyeToHand(nullptr) - , m_groupEyeToHand(nullptr) - , m_tableEyeInHand(nullptr) - , m_groupEyeInHand(nullptr) - , m_btnEyeToHandAddRow(nullptr) - , m_btnEyeToHandDeleteRow(nullptr) - , m_btnEyeToHandCalib(nullptr) - , m_btnEyeInHandAddRow(nullptr) - , m_btnEyeInHandDeleteRow(nullptr) - , m_btnEyeInHandCalib(nullptr) - , m_groupTCPCalib(nullptr) - , m_tableTCP(nullptr) - , m_tcpModeCombo(nullptr) - , m_tcpEulerOrderCombo(nullptr) - , m_tcpRefPoseIndex(nullptr) - , m_tcpWorldRx(nullptr) - , m_tcpWorldRy(nullptr) - , m_tcpWorldRz(nullptr) - , m_tcpOrientationGroup(nullptr) - , m_tcpAddRowBtn(nullptr) - , m_tcpRemoveRowBtn(nullptr) - , m_btnTCPCalib(nullptr) - , m_inputEyeX(nullptr) - , m_inputEyeY(nullptr) - , m_inputEyeZ(nullptr) - , m_inputRobotX(nullptr) - , m_inputRobotY(nullptr) - , m_inputRobotZ(nullptr) - , m_btnEyeToHandAddInput(nullptr) - , m_inputEndX(nullptr) - , m_inputEndY(nullptr) - , m_inputEndZ(nullptr) - , m_inputEndRoll(nullptr) - , m_inputEndPitch(nullptr) - , m_inputEndYaw(nullptr) - , m_inputCamX(nullptr) - , m_inputCamY(nullptr) - , m_inputCamZ(nullptr) - , m_btnEyeInHandAddInput(nullptr) - , m_inputTcpX(nullptr) - , m_inputTcpY(nullptr) - , m_inputTcpZ(nullptr) - , m_inputTcpRx(nullptr) - , m_inputTcpRy(nullptr) - , m_inputTcpRz(nullptr) - , m_btnTcpAddInput(nullptr) -{ - setupUI(); - SpinBoxPasteHelper::install(this); -} - -CalibDataWidget::~CalibDataWidget() -{ -} - -void CalibDataWidget::setupUI() -{ - QVBoxLayout* mainLayout = new QVBoxLayout(this); - - // 标定模式选择 - QHBoxLayout* modeLayout = new QHBoxLayout(); - QLabel* lblMode = new QLabel("标定模式:", this); - m_cbCalibType = new QComboBox(this); - m_cbCalibType->addItem("Eye-To-Hand (眼在手外)"); - m_cbCalibType->addItem("Eye-In-Hand (眼在手上)"); - m_cbCalibType->addItem("TCP 标定"); - connect(m_cbCalibType, QOverload::of(&QComboBox::currentIndexChanged), - this, &CalibDataWidget::onCalibTypeChanged); - modeLayout->addWidget(lblMode); - modeLayout->addWidget(m_cbCalibType); - modeLayout->addStretch(); - mainLayout->addLayout(modeLayout); - - // Eye-To-Hand 数据组 - m_groupEyeToHand = createEyeToHandGroup(); - mainLayout->addWidget(m_groupEyeToHand, 1); - - // Eye-In-Hand 数据组 - m_groupEyeInHand = createEyeInHandGroup(); - mainLayout->addWidget(m_groupEyeInHand, 1); - m_groupEyeInHand->setVisible(false); - - // TCP 标定数据组 - m_groupTCPCalib = createTCPCalibGroup(); - mainLayout->addWidget(m_groupTCPCalib, 1); - m_groupTCPCalib->setVisible(false); - - // 初始化按钮启用状态 - onCalibTypeChanged(0); -} - -QWidget* CalibDataWidget::createEyeToHandGroup() -{ - QWidget* group = new QWidget(this); - QVBoxLayout* layout = new QVBoxLayout(group); - layout->setContentsMargins(0, 0, 0, 0); - - m_tableEyeToHand = new QTableWidget(this); - m_tableEyeToHand->setColumnCount(6); - m_tableEyeToHand->setHorizontalHeaderLabels({ - "Eye X", "Eye Y", "Eye Z", "Robot X", "Robot Y", "Robot Z" - }); - m_tableEyeToHand->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); - m_tableEyeToHand->setSelectionBehavior(QAbstractItemView::SelectRows); - - layout->addWidget(m_tableEyeToHand, 1); - - // 内联按钮行 - QHBoxLayout* btnLayout = new QHBoxLayout(); - m_btnEyeToHandAddRow = new QPushButton("添加行", this); - m_btnEyeToHandDeleteRow = new QPushButton("删除行", this); - m_btnEyeToHandCalib = new QPushButton("Eye-To-Hand 标定", this); - - connect(m_btnEyeToHandAddRow, &QPushButton::clicked, this, [this]() { - int row = m_tableEyeToHand->rowCount(); - m_tableEyeToHand->insertRow(row); - for (int col = 0; col < 6; ++col) { - m_tableEyeToHand->setItem(row, col, new QTableWidgetItem("0")); - } - m_tableEyeToHand->scrollToBottom(); - }); - connect(m_btnEyeToHandDeleteRow, &QPushButton::clicked, this, [this]() { - int row = m_tableEyeToHand->currentRow(); - if (row >= 0) { - m_tableEyeToHand->removeRow(row); - } - }); - connect(m_btnEyeToHandCalib, &QPushButton::clicked, this, &CalibDataWidget::requestEyeToHandCalib); - - btnLayout->addWidget(m_btnEyeToHandAddRow); - btnLayout->addWidget(m_btnEyeToHandDeleteRow); - btnLayout->addWidget(m_btnEyeToHandCalib); - btnLayout->addStretch(); - layout->addLayout(btnLayout); - - // 数据输入区 - QGroupBox* inputGroup = new QGroupBox("数据输入", group); - QGridLayout* inputLayout = new QGridLayout(inputGroup); - inputLayout->setHorizontalSpacing(2); - inputLayout->setVerticalSpacing(4); - inputLayout->setContentsMargins(4, 4, 4, 4); - inputLayout->setColumnStretch(0, 0); - inputLayout->setColumnStretch(1, 1); - inputLayout->setColumnStretch(2, 0); - inputLayout->setColumnStretch(3, 1); - inputLayout->setColumnStretch(4, 0); - inputLayout->setColumnStretch(5, 1); - inputLayout->setColumnStretch(6, 0); - - auto createSpinBox = [this]() { - QDoubleSpinBox* sb = new QDoubleSpinBox(this); - sb->setRange(-10000, 10000); - sb->setDecimals(3); - return sb; - }; - - // 第1行:Eye X/Y/Z - inputLayout->addWidget(new QLabel("Eye X:", this), 0, 0); - m_inputEyeX = createSpinBox(); - inputLayout->addWidget(m_inputEyeX, 0, 1); - inputLayout->addWidget(new QLabel("Y:", this), 0, 2); - m_inputEyeY = createSpinBox(); - inputLayout->addWidget(m_inputEyeY, 0, 3); - inputLayout->addWidget(new QLabel("Z:", this), 0, 4); - m_inputEyeZ = createSpinBox(); - inputLayout->addWidget(m_inputEyeZ, 0, 5); - - // 第2行:Robot X/Y/Z + 添加按钮 - inputLayout->addWidget(new QLabel("Robot X:", this), 1, 0); - m_inputRobotX = createSpinBox(); - inputLayout->addWidget(m_inputRobotX, 1, 1); - inputLayout->addWidget(new QLabel("Y:", this), 1, 2); - m_inputRobotY = createSpinBox(); - inputLayout->addWidget(m_inputRobotY, 1, 3); - inputLayout->addWidget(new QLabel("Z:", this), 1, 4); - m_inputRobotZ = createSpinBox(); - inputLayout->addWidget(m_inputRobotZ, 1, 5); - - m_btnEyeToHandAddInput = new QPushButton("添加到表格", this); - connect(m_btnEyeToHandAddInput, &QPushButton::clicked, this, [this]() { - int row = m_tableEyeToHand->rowCount(); - m_tableEyeToHand->insertRow(row); - m_tableEyeToHand->setItem(row, 0, new QTableWidgetItem(QString::number(m_inputEyeX->value(), 'f', 3))); - m_tableEyeToHand->setItem(row, 1, new QTableWidgetItem(QString::number(m_inputEyeY->value(), 'f', 3))); - m_tableEyeToHand->setItem(row, 2, new QTableWidgetItem(QString::number(m_inputEyeZ->value(), 'f', 3))); - m_tableEyeToHand->setItem(row, 3, new QTableWidgetItem(QString::number(m_inputRobotX->value(), 'f', 3))); - m_tableEyeToHand->setItem(row, 4, new QTableWidgetItem(QString::number(m_inputRobotY->value(), 'f', 3))); - m_tableEyeToHand->setItem(row, 5, new QTableWidgetItem(QString::number(m_inputRobotZ->value(), 'f', 3))); - m_tableEyeToHand->scrollToBottom(); - }); - inputLayout->addWidget(m_btnEyeToHandAddInput, 1, 6); - - layout->addWidget(inputGroup); - - return group; -} - -QWidget* CalibDataWidget::createEyeInHandGroup() -{ - QWidget* group = new QWidget(this); - QVBoxLayout* layout = new QVBoxLayout(group); - layout->setContentsMargins(0, 0, 0, 0); - - m_tableEyeInHand = new QTableWidget(this); - m_tableEyeInHand->setColumnCount(9); - m_tableEyeInHand->setHorizontalHeaderLabels({ - "End X", "End Y", "End Z", "End Roll", "End Pitch", "End Yaw", - "Cam X", "Cam Y", "Cam Z" - }); - m_tableEyeInHand->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); - m_tableEyeInHand->setSelectionBehavior(QAbstractItemView::SelectRows); - - layout->addWidget(m_tableEyeInHand, 1); - - // 内联按钮行 - QHBoxLayout* btnLayout = new QHBoxLayout(); - m_btnEyeInHandAddRow = new QPushButton("添加行", this); - m_btnEyeInHandDeleteRow = new QPushButton("删除行", this); - m_btnEyeInHandCalib = new QPushButton("Eye-In-Hand 标定", this); - - connect(m_btnEyeInHandAddRow, &QPushButton::clicked, this, [this]() { - int row = m_tableEyeInHand->rowCount(); - m_tableEyeInHand->insertRow(row); - for (int col = 0; col < 9; ++col) { - m_tableEyeInHand->setItem(row, col, new QTableWidgetItem("0")); - } - m_tableEyeInHand->scrollToBottom(); - }); - connect(m_btnEyeInHandDeleteRow, &QPushButton::clicked, this, [this]() { - int row = m_tableEyeInHand->currentRow(); - if (row >= 0) { - m_tableEyeInHand->removeRow(row); - } - }); - connect(m_btnEyeInHandCalib, &QPushButton::clicked, this, &CalibDataWidget::requestEyeInHandCalib); - - btnLayout->addWidget(m_btnEyeInHandAddRow); - btnLayout->addWidget(m_btnEyeInHandDeleteRow); - btnLayout->addWidget(m_btnEyeInHandCalib); - btnLayout->addStretch(); - layout->addLayout(btnLayout); - - // 数据输入区 - QGroupBox* inputGroup = new QGroupBox("数据输入", group); - QGridLayout* inputLayout = new QGridLayout(inputGroup); - inputLayout->setHorizontalSpacing(2); - inputLayout->setVerticalSpacing(4); - inputLayout->setContentsMargins(4, 4, 4, 4); - for (int c = 0; c <= 11; ++c) - inputLayout->setColumnStretch(c, (c % 2 == 1) ? 1 : 0); - - auto createSpinBox = [this]() { - QDoubleSpinBox* sb = new QDoubleSpinBox(this); - sb->setRange(-10000, 10000); - sb->setDecimals(3); - return sb; - }; - - // 第1行:末端位姿 - inputLayout->addWidget(new QLabel("End X:", this), 0, 0); - m_inputEndX = createSpinBox(); - inputLayout->addWidget(m_inputEndX, 0, 1); - inputLayout->addWidget(new QLabel("Y:", this), 0, 2); - m_inputEndY = createSpinBox(); - inputLayout->addWidget(m_inputEndY, 0, 3); - inputLayout->addWidget(new QLabel("Z:", this), 0, 4); - m_inputEndZ = createSpinBox(); - inputLayout->addWidget(m_inputEndZ, 0, 5); - inputLayout->addWidget(new QLabel("Roll\302\260:", this), 0, 6); - m_inputEndRoll = createSpinBox(); - inputLayout->addWidget(m_inputEndRoll, 0, 7); - inputLayout->addWidget(new QLabel("Pitch\302\260:", this), 0, 8); - m_inputEndPitch = createSpinBox(); - inputLayout->addWidget(m_inputEndPitch, 0, 9); - inputLayout->addWidget(new QLabel("Yaw\302\260:", this), 0, 10); - m_inputEndYaw = createSpinBox(); - inputLayout->addWidget(m_inputEndYaw, 0, 11); - - // 第2行:相机观测点 + 添加按钮 - inputLayout->addWidget(new QLabel("Cam X:", this), 1, 0); - m_inputCamX = createSpinBox(); - inputLayout->addWidget(m_inputCamX, 1, 1); - inputLayout->addWidget(new QLabel("Y:", this), 1, 2); - m_inputCamY = createSpinBox(); - inputLayout->addWidget(m_inputCamY, 1, 3); - inputLayout->addWidget(new QLabel("Z:", this), 1, 4); - m_inputCamZ = createSpinBox(); - inputLayout->addWidget(m_inputCamZ, 1, 5); - - m_btnEyeInHandAddInput = new QPushButton("添加到表格", this); - connect(m_btnEyeInHandAddInput, &QPushButton::clicked, this, [this]() { - int row = m_tableEyeInHand->rowCount(); - m_tableEyeInHand->insertRow(row); - m_tableEyeInHand->setItem(row, 0, new QTableWidgetItem(QString::number(m_inputEndX->value(), 'f', 3))); - m_tableEyeInHand->setItem(row, 1, new QTableWidgetItem(QString::number(m_inputEndY->value(), 'f', 3))); - m_tableEyeInHand->setItem(row, 2, new QTableWidgetItem(QString::number(m_inputEndZ->value(), 'f', 3))); - m_tableEyeInHand->setItem(row, 3, new QTableWidgetItem(QString::number(m_inputEndRoll->value(), 'f', 3))); - m_tableEyeInHand->setItem(row, 4, new QTableWidgetItem(QString::number(m_inputEndPitch->value(), 'f', 3))); - m_tableEyeInHand->setItem(row, 5, new QTableWidgetItem(QString::number(m_inputEndYaw->value(), 'f', 3))); - m_tableEyeInHand->setItem(row, 6, new QTableWidgetItem(QString::number(m_inputCamX->value(), 'f', 3))); - m_tableEyeInHand->setItem(row, 7, new QTableWidgetItem(QString::number(m_inputCamY->value(), 'f', 3))); - m_tableEyeInHand->setItem(row, 8, new QTableWidgetItem(QString::number(m_inputCamZ->value(), 'f', 3))); - m_tableEyeInHand->scrollToBottom(); - }); - inputLayout->addWidget(m_btnEyeInHandAddInput, 1, 10, 1, 2); - - layout->addWidget(inputGroup); - - return group; -} - -void CalibDataWidget::updateTableVisibility() -{ - int index = m_cbCalibType->currentIndex(); - m_groupEyeToHand->setVisible(index == 0); - m_groupEyeInHand->setVisible(index == 1); - m_groupTCPCalib->setVisible(index == 2); -} - -void CalibDataWidget::onCalibTypeChanged(int index) -{ - updateTableVisibility(); - - // Eye-To-Hand 按钮 - m_btnEyeToHandCalib->setEnabled(index == 0); - m_btnEyeToHandAddRow->setEnabled(index == 0); - m_btnEyeToHandDeleteRow->setEnabled(index == 0); - - // Eye-In-Hand 按钮 - m_btnEyeInHandCalib->setEnabled(index == 1); - m_btnEyeInHandAddRow->setEnabled(index == 1); - m_btnEyeInHandDeleteRow->setEnabled(index == 1); - - // TCP 按钮 - m_tcpAddRowBtn->setEnabled(index == 2); - m_tcpRemoveRowBtn->setEnabled(index == 2); - m_btnTCPCalib->setEnabled(index == 2); - - if (index <= 1) { - emit calibTypeChanged(index == 0 ? HECCalibrationType::EyeToHand : HECCalibrationType::EyeInHand); - } -} - -void CalibDataWidget::getEyeToHandData(std::vector& eyePoints, - std::vector& robotPoints) const -{ - eyePoints.clear(); - robotPoints.clear(); - - for (int row = 0; row < m_tableEyeToHand->rowCount(); ++row) { - HECPoint3D eyePt, robotPt; - - QTableWidgetItem* item0 = m_tableEyeToHand->item(row, 0); - QTableWidgetItem* item1 = m_tableEyeToHand->item(row, 1); - QTableWidgetItem* item2 = m_tableEyeToHand->item(row, 2); - QTableWidgetItem* item3 = m_tableEyeToHand->item(row, 3); - QTableWidgetItem* item4 = m_tableEyeToHand->item(row, 4); - QTableWidgetItem* item5 = m_tableEyeToHand->item(row, 5); - - if (item0 && item1 && item2 && item3 && item4 && item5) { - eyePt.x = item0->text().toDouble(); - eyePt.y = item1->text().toDouble(); - eyePt.z = item2->text().toDouble(); - robotPt.x = item3->text().toDouble(); - robotPt.y = item4->text().toDouble(); - robotPt.z = item5->text().toDouble(); - - eyePoints.push_back(eyePt); - robotPoints.push_back(robotPt); - } - } -} - -void CalibDataWidget::getEyeInHandData(std::vector& calibData) const -{ - calibData.clear(); - - const double deg2rad = M_PI / 180.0; - - for (int row = 0; row < m_tableEyeInHand->rowCount(); ++row) { - HECEyeInHandData data; - - // 获取末端位姿 - double endX = m_tableEyeInHand->item(row, 0) ? - m_tableEyeInHand->item(row, 0)->text().toDouble() : 0; - double endY = m_tableEyeInHand->item(row, 1) ? - m_tableEyeInHand->item(row, 1)->text().toDouble() : 0; - double endZ = m_tableEyeInHand->item(row, 2) ? - m_tableEyeInHand->item(row, 2)->text().toDouble() : 0; - double endRoll = m_tableEyeInHand->item(row, 3) ? - m_tableEyeInHand->item(row, 3)->text().toDouble() * deg2rad : 0; - double endPitch = m_tableEyeInHand->item(row, 4) ? - m_tableEyeInHand->item(row, 4)->text().toDouble() * deg2rad : 0; - double endYaw = m_tableEyeInHand->item(row, 5) ? - m_tableEyeInHand->item(row, 5)->text().toDouble() * deg2rad : 0; - - // 构建末端位姿矩阵 - // 外旋 ZYX: R = Rx(roll) * Ry(pitch) * Rz(yaw) - HECRotationMatrix R; - double cr = cos(endRoll), sr = sin(endRoll); - double cp = cos(endPitch), sp = sin(endPitch); - double cy = cos(endYaw), sy = sin(endYaw); - - R.at(0, 0) = cp * cy; - R.at(0, 1) = -cp * sy; - R.at(0, 2) = sp; - R.at(1, 0) = sr * sp * cy + cr * sy; - R.at(1, 1) = -sr * sp * sy + cr * cy; - R.at(1, 2) = -sr * cp; - R.at(2, 0) = -cr * sp * cy + sr * sy; - R.at(2, 1) = cr * sp * sy + sr * cy; - R.at(2, 2) = cr * cp; - - HECTranslationVector T(endX, endY, endZ); - data.endPose = HECHomogeneousMatrix(R, T); - - // 获取相机观测点 - data.targetInCamera.x = m_tableEyeInHand->item(row, 6) ? - m_tableEyeInHand->item(row, 6)->text().toDouble() : 0; - data.targetInCamera.y = m_tableEyeInHand->item(row, 7) ? - m_tableEyeInHand->item(row, 7)->text().toDouble() : 0; - data.targetInCamera.z = m_tableEyeInHand->item(row, 8) ? - m_tableEyeInHand->item(row, 8)->text().toDouble() : 0; - - calibData.push_back(data); - } -} - -HECCalibrationType CalibDataWidget::getCalibType() const -{ - return m_cbCalibType->currentIndex() == 0 ? - HECCalibrationType::EyeToHand : HECCalibrationType::EyeInHand; -} - -void CalibDataWidget::clearAll() -{ - m_tableEyeToHand->setRowCount(0); - m_tableEyeInHand->setRowCount(0); - m_tableTCP->setRowCount(0); -} - -QWidget* CalibDataWidget::createTCPCalibGroup() -{ - QWidget* group = new QWidget(this); - QVBoxLayout* layout = new QVBoxLayout(group); - layout->setContentsMargins(0, 0, 0, 0); - - // 模式选择行 - QHBoxLayout* modeLayout = new QHBoxLayout(); - modeLayout->addWidget(new QLabel("标定模式:", this)); - m_tcpModeCombo = new QComboBox(this); - m_tcpModeCombo->addItem("位置标定 (3-DOF)"); - m_tcpModeCombo->addItem("完整标定 (6-DOF)"); - connect(m_tcpModeCombo, QOverload::of(&QComboBox::currentIndexChanged), - this, &CalibDataWidget::onTCPModeChanged); - modeLayout->addWidget(m_tcpModeCombo); - - modeLayout->addWidget(new QLabel("欧拉角顺序:", this)); - m_tcpEulerOrderCombo = new QComboBox(this); - m_tcpEulerOrderCombo->addItem("XYZ", static_cast(HECEulerOrder::XYZ)); - m_tcpEulerOrderCombo->addItem("XZY", static_cast(HECEulerOrder::XZY)); - m_tcpEulerOrderCombo->addItem("YXZ", static_cast(HECEulerOrder::YXZ)); - m_tcpEulerOrderCombo->addItem("YZX", static_cast(HECEulerOrder::YZX)); - m_tcpEulerOrderCombo->addItem("ZXY", static_cast(HECEulerOrder::ZXY)); - m_tcpEulerOrderCombo->addItem("ZYX (常用)", static_cast(HECEulerOrder::ZYX)); - m_tcpEulerOrderCombo->setCurrentIndex(5); - modeLayout->addWidget(m_tcpEulerOrderCombo); - modeLayout->addStretch(); - layout->addLayout(modeLayout); - - // 法兰位姿表格 - m_tableTCP = new QTableWidget(this); - m_tableTCP->setColumnCount(6); - m_tableTCP->setHorizontalHeaderLabels({"X", "Y", "Z", "Roll(°)", "Pitch(°)", "Yaw(°)"}); - m_tableTCP->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); - m_tableTCP->setSelectionBehavior(QAbstractItemView::SelectRows); - layout->addWidget(m_tableTCP, 1); - - // 按钮行(含 TCP 标定按钮) - QHBoxLayout* btnLayout = new QHBoxLayout(); - m_tcpAddRowBtn = new QPushButton("添加行", this); - m_tcpRemoveRowBtn = new QPushButton("删除行", this); - m_btnTCPCalib = new QPushButton("TCP 标定", this); - connect(m_tcpAddRowBtn, &QPushButton::clicked, this, &CalibDataWidget::onTCPAddRow); - connect(m_tcpRemoveRowBtn, &QPushButton::clicked, this, &CalibDataWidget::onTCPRemoveRow); - connect(m_btnTCPCalib, &QPushButton::clicked, this, &CalibDataWidget::requestTCPCalib); - btnLayout->addWidget(m_tcpAddRowBtn); - btnLayout->addWidget(m_tcpRemoveRowBtn); - btnLayout->addWidget(m_btnTCPCalib); - btnLayout->addStretch(); - layout->addLayout(btnLayout); - - // 数据输入区 - QGroupBox* inputGroup = new QGroupBox("数据输入", group); - QGridLayout* inputLayout = new QGridLayout(inputGroup); - inputLayout->setHorizontalSpacing(2); - inputLayout->setVerticalSpacing(4); - inputLayout->setContentsMargins(4, 4, 4, 4); - inputLayout->setColumnStretch(0, 0); - inputLayout->setColumnStretch(1, 1); - inputLayout->setColumnStretch(2, 0); - inputLayout->setColumnStretch(3, 1); - inputLayout->setColumnStretch(4, 0); - inputLayout->setColumnStretch(5, 1); - inputLayout->setColumnStretch(6, 0); - - auto createSpinBox = [this]() { - QDoubleSpinBox* sb = new QDoubleSpinBox(this); - sb->setRange(-10000, 10000); - sb->setDecimals(3); - return sb; - }; - - // 第1行:X/Y/Z - inputLayout->addWidget(new QLabel("X:", this), 0, 0); - m_inputTcpX = createSpinBox(); - inputLayout->addWidget(m_inputTcpX, 0, 1); - inputLayout->addWidget(new QLabel("Y:", this), 0, 2); - m_inputTcpY = createSpinBox(); - inputLayout->addWidget(m_inputTcpY, 0, 3); - inputLayout->addWidget(new QLabel("Z:", this), 0, 4); - m_inputTcpZ = createSpinBox(); - inputLayout->addWidget(m_inputTcpZ, 0, 5); - - // 第2行:Roll/Pitch/Yaw + 添加按钮 - inputLayout->addWidget(new QLabel("Roll\302\260:", this), 1, 0); - m_inputTcpRx = createSpinBox(); - inputLayout->addWidget(m_inputTcpRx, 1, 1); - inputLayout->addWidget(new QLabel("Pitch\302\260:", this), 1, 2); - m_inputTcpRy = createSpinBox(); - inputLayout->addWidget(m_inputTcpRy, 1, 3); - inputLayout->addWidget(new QLabel("Yaw\302\260:", this), 1, 4); - m_inputTcpRz = createSpinBox(); - inputLayout->addWidget(m_inputTcpRz, 1, 5); - - m_btnTcpAddInput = new QPushButton("添加到表格", this); - connect(m_btnTcpAddInput, &QPushButton::clicked, this, [this]() { - int row = m_tableTCP->rowCount(); - m_tableTCP->insertRow(row); - m_tableTCP->setItem(row, 0, new QTableWidgetItem(QString::number(m_inputTcpX->value(), 'f', 3))); - m_tableTCP->setItem(row, 1, new QTableWidgetItem(QString::number(m_inputTcpY->value(), 'f', 3))); - m_tableTCP->setItem(row, 2, new QTableWidgetItem(QString::number(m_inputTcpZ->value(), 'f', 3))); - m_tableTCP->setItem(row, 3, new QTableWidgetItem(QString::number(m_inputTcpRx->value(), 'f', 3))); - m_tableTCP->setItem(row, 4, new QTableWidgetItem(QString::number(m_inputTcpRy->value(), 'f', 3))); - m_tableTCP->setItem(row, 5, new QTableWidgetItem(QString::number(m_inputTcpRz->value(), 'f', 3))); - m_tableTCP->scrollToBottom(); - }); - inputLayout->addWidget(m_btnTcpAddInput, 1, 6); - - layout->addWidget(inputGroup); - - // 6-DOF 姿态参数组(默认隐藏) - m_tcpOrientationGroup = new QGroupBox("6-DOF 姿态参数", this); - QGridLayout* oriLayout = new QGridLayout(m_tcpOrientationGroup); - - oriLayout->addWidget(new QLabel("参考位姿索引:", this), 0, 0); - m_tcpRefPoseIndex = new QSpinBox(this); - m_tcpRefPoseIndex->setRange(0, 999); - m_tcpRefPoseIndex->setValue(0); - oriLayout->addWidget(m_tcpRefPoseIndex, 0, 1); - - oriLayout->addWidget(new QLabel("期望世界朝向 Rx(°):", this), 1, 0); - m_tcpWorldRx = new QDoubleSpinBox(this); - m_tcpWorldRx->setRange(-360, 360); - m_tcpWorldRx->setDecimals(2); - oriLayout->addWidget(m_tcpWorldRx, 1, 1); - - oriLayout->addWidget(new QLabel("Ry(°):", this), 1, 2); - m_tcpWorldRy = new QDoubleSpinBox(this); - m_tcpWorldRy->setRange(-360, 360); - m_tcpWorldRy->setDecimals(2); - oriLayout->addWidget(m_tcpWorldRy, 1, 3); - - oriLayout->addWidget(new QLabel("Rz(°):", this), 1, 4); - m_tcpWorldRz = new QDoubleSpinBox(this); - m_tcpWorldRz->setRange(-360, 360); - m_tcpWorldRz->setDecimals(2); - oriLayout->addWidget(m_tcpWorldRz, 1, 5); - - m_tcpOrientationGroup->setVisible(false); - layout->addWidget(m_tcpOrientationGroup); - - return group; -} - -void CalibDataWidget::onTCPModeChanged(int index) -{ - m_tcpOrientationGroup->setVisible(index == 1); -} - -void CalibDataWidget::onTCPAddRow() -{ - int row = m_tableTCP->rowCount(); - m_tableTCP->insertRow(row); - for (int col = 0; col < 6; ++col) { - QTableWidgetItem* item = new QTableWidgetItem("0"); - m_tableTCP->setItem(row, col, item); - } - m_tableTCP->scrollToBottom(); -} - -void CalibDataWidget::onTCPRemoveRow() -{ - int row = m_tableTCP->currentRow(); - if (row >= 0) { - m_tableTCP->removeRow(row); - } -} - -HECTCPCalibData CalibDataWidget::getTCPCalibData() const -{ - HECTCPCalibData data; - - // 标定模式 - data.mode = (m_tcpModeCombo->currentIndex() == 0) ? - HECTCPCalibMode::PositionOnly : HECTCPCalibMode::Full6DOF; - - // 欧拉角顺序 - HECEulerOrder eulerOrder = static_cast( - m_tcpEulerOrderCombo->currentData().toInt()); - - // 读取表格中的法兰位姿 - for (int row = 0; row < m_tableTCP->rowCount(); ++row) { - HECTCPCalibPose pose; - pose.x = m_tableTCP->item(row, 0) ? m_tableTCP->item(row, 0)->text().toDouble() : 0; - pose.y = m_tableTCP->item(row, 1) ? m_tableTCP->item(row, 1)->text().toDouble() : 0; - pose.z = m_tableTCP->item(row, 2) ? m_tableTCP->item(row, 2)->text().toDouble() : 0; - pose.rx = m_tableTCP->item(row, 3) ? m_tableTCP->item(row, 3)->text().toDouble() : 0; - pose.ry = m_tableTCP->item(row, 4) ? m_tableTCP->item(row, 4)->text().toDouble() : 0; - pose.rz = m_tableTCP->item(row, 5) ? m_tableTCP->item(row, 5)->text().toDouble() : 0; - pose.eulerOrder = eulerOrder; - data.poses.push_back(pose); - } - - // 6-DOF 参数 - data.referencePoseIndex = m_tcpRefPoseIndex->value(); - data.worldRx = m_tcpWorldRx->value(); - data.worldRy = m_tcpWorldRy->value(); - data.worldRz = m_tcpWorldRz->value(); - data.worldEulerOrder = eulerOrder; - - return data; -} - -void CalibDataWidget::setRobotInput(double x, double y, double z, - double rx, double ry, double rz) -{ - int mode = m_cbCalibType->currentIndex(); - if (mode == 0) { - // Eye-To-Hand: 仅填充 Robot X/Y/Z - m_inputRobotX->setValue(x); - m_inputRobotY->setValue(y); - m_inputRobotZ->setValue(z); - } else if (mode == 1) { - // Eye-In-Hand: 填充末端位姿 - m_inputEndX->setValue(x); - m_inputEndY->setValue(y); - m_inputEndZ->setValue(z); - m_inputEndRoll->setValue(rx); - m_inputEndPitch->setValue(ry); - m_inputEndYaw->setValue(rz); - } else if (mode == 2) { - // TCP: 填充位姿 - m_inputTcpX->setValue(x); - m_inputTcpY->setValue(y); - m_inputTcpZ->setValue(z); - m_inputTcpRx->setValue(rx); - m_inputTcpRy->setValue(ry); - m_inputTcpRz->setValue(rz); - } -} - -void CalibDataWidget::setCameraInput(double x, double y, double z, - double rx, double ry, double rz) -{ - int mode = m_cbCalibType->currentIndex(); - if (mode == 0) { - // Eye-To-Hand: 填充相机坐标 Eye X/Y/Z - m_inputEyeX->setValue(x); - m_inputEyeY->setValue(y); - m_inputEyeZ->setValue(z); - } else if (mode == 1) { - // Eye-In-Hand: 填充相机观测点 - m_inputCamX->setValue(x); - m_inputCamY->setValue(y); - m_inputCamZ->setValue(z); - } - // TCP 模式不需要相机输入 -} +#include "CalibDataWidget.h" +#include "../../SpinBoxPasteHelper.h" + +#include +#include +#include +#include +#include +#include +#include + +CalibDataWidget::CalibDataWidget(QWidget* parent) + : QWidget(parent) + , m_cbCalibType(nullptr) + , m_tableEyeToHand(nullptr) + , m_groupEyeToHand(nullptr) + , m_tableEyeInHand(nullptr) + , m_groupEyeInHand(nullptr) + , m_btnEyeToHandAddRow(nullptr) + , m_btnEyeToHandDeleteRow(nullptr) + , m_btnEyeToHandCalib(nullptr) + , m_btnEyeInHandAddRow(nullptr) + , m_btnEyeInHandDeleteRow(nullptr) + , m_btnEyeInHandCalib(nullptr) + , m_groupTCPCalib(nullptr) + , m_tableTCP(nullptr) + , m_tcpModeCombo(nullptr) + , m_tcpEulerOrderCombo(nullptr) + , m_tcpRefPoseIndex(nullptr) + , m_tcpWorldRx(nullptr) + , m_tcpWorldRy(nullptr) + , m_tcpWorldRz(nullptr) + , m_tcpOrientationGroup(nullptr) + , m_tcpAddRowBtn(nullptr) + , m_tcpRemoveRowBtn(nullptr) + , m_btnTCPCalib(nullptr) + , m_inputEyeX(nullptr) + , m_inputEyeY(nullptr) + , m_inputEyeZ(nullptr) + , m_inputRobotX(nullptr) + , m_inputRobotY(nullptr) + , m_inputRobotZ(nullptr) + , m_btnEyeToHandAddInput(nullptr) + , m_inputEndX(nullptr) + , m_inputEndY(nullptr) + , m_inputEndZ(nullptr) + , m_inputEndRoll(nullptr) + , m_inputEndPitch(nullptr) + , m_inputEndYaw(nullptr) + , m_inputCamX(nullptr) + , m_inputCamY(nullptr) + , m_inputCamZ(nullptr) + , m_btnEyeInHandAddInput(nullptr) + , m_inputTcpX(nullptr) + , m_inputTcpY(nullptr) + , m_inputTcpZ(nullptr) + , m_inputTcpRx(nullptr) + , m_inputTcpRy(nullptr) + , m_inputTcpRz(nullptr) + , m_btnTcpAddInput(nullptr) +{ + setupUI(); + SpinBoxPasteHelper::install(this); +} + +CalibDataWidget::~CalibDataWidget() +{ +} + +void CalibDataWidget::setupUI() +{ + QVBoxLayout* mainLayout = new QVBoxLayout(this); + + // 标定模式选择 + QHBoxLayout* modeLayout = new QHBoxLayout(); + QLabel* lblMode = new QLabel("标定模式:", this); + m_cbCalibType = new QComboBox(this); + m_cbCalibType->addItem("Eye-To-Hand (眼在手外)"); + m_cbCalibType->addItem("Eye-In-Hand (眼在手上)"); + m_cbCalibType->addItem("TCP 标定"); + connect(m_cbCalibType, QOverload::of(&QComboBox::currentIndexChanged), + this, &CalibDataWidget::onCalibTypeChanged); + modeLayout->addWidget(lblMode); + modeLayout->addWidget(m_cbCalibType); + modeLayout->addStretch(); + mainLayout->addLayout(modeLayout); + + // Eye-To-Hand 数据组 + m_groupEyeToHand = createEyeToHandGroup(); + mainLayout->addWidget(m_groupEyeToHand, 1); + + // Eye-In-Hand 数据组 + m_groupEyeInHand = createEyeInHandGroup(); + mainLayout->addWidget(m_groupEyeInHand, 1); + m_groupEyeInHand->setVisible(false); + + // TCP 标定数据组 + m_groupTCPCalib = createTCPCalibGroup(); + mainLayout->addWidget(m_groupTCPCalib, 1); + m_groupTCPCalib->setVisible(false); + + // 初始化按钮启用状态 + onCalibTypeChanged(0); +} + +QWidget* CalibDataWidget::createEyeToHandGroup() +{ + QWidget* group = new QWidget(this); + QVBoxLayout* layout = new QVBoxLayout(group); + layout->setContentsMargins(0, 0, 0, 0); + + m_tableEyeToHand = new QTableWidget(this); + m_tableEyeToHand->setColumnCount(6); + m_tableEyeToHand->setHorizontalHeaderLabels({ + "Eye X", "Eye Y", "Eye Z", "Robot X", "Robot Y", "Robot Z" + }); + m_tableEyeToHand->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + m_tableEyeToHand->setSelectionBehavior(QAbstractItemView::SelectRows); + + layout->addWidget(m_tableEyeToHand, 1); + + // 内联按钮行 + QHBoxLayout* btnLayout = new QHBoxLayout(); + m_btnEyeToHandAddRow = new QPushButton("添加行", this); + m_btnEyeToHandDeleteRow = new QPushButton("删除行", this); + m_btnEyeToHandCalib = new QPushButton("Eye-To-Hand 标定", this); + + connect(m_btnEyeToHandAddRow, &QPushButton::clicked, this, [this]() { + int row = m_tableEyeToHand->rowCount(); + m_tableEyeToHand->insertRow(row); + for (int col = 0; col < 6; ++col) { + m_tableEyeToHand->setItem(row, col, new QTableWidgetItem("0")); + } + m_tableEyeToHand->scrollToBottom(); + }); + connect(m_btnEyeToHandDeleteRow, &QPushButton::clicked, this, [this]() { + int row = m_tableEyeToHand->currentRow(); + if (row >= 0) { + m_tableEyeToHand->removeRow(row); + } + }); + connect(m_btnEyeToHandCalib, &QPushButton::clicked, this, &CalibDataWidget::requestEyeToHandCalib); + + btnLayout->addWidget(m_btnEyeToHandAddRow); + btnLayout->addWidget(m_btnEyeToHandDeleteRow); + btnLayout->addWidget(m_btnEyeToHandCalib); + btnLayout->addStretch(); + layout->addLayout(btnLayout); + + // 数据输入区 + QGroupBox* inputGroup = new QGroupBox("数据输入", group); + QGridLayout* inputLayout = new QGridLayout(inputGroup); + inputLayout->setHorizontalSpacing(2); + inputLayout->setVerticalSpacing(4); + inputLayout->setContentsMargins(4, 4, 4, 4); + inputLayout->setColumnStretch(0, 0); + inputLayout->setColumnStretch(1, 1); + inputLayout->setColumnStretch(2, 0); + inputLayout->setColumnStretch(3, 1); + inputLayout->setColumnStretch(4, 0); + inputLayout->setColumnStretch(5, 1); + + auto createSpinBox = [this]() { + QDoubleSpinBox* sb = new QDoubleSpinBox(this); + sb->setRange(-10000, 10000); + sb->setDecimals(3); + return sb; + }; + + // 第1行:Eye X/Y/Z + inputLayout->addWidget(new QLabel("Eye X:", this), 0, 0); + m_inputEyeX = createSpinBox(); + inputLayout->addWidget(m_inputEyeX, 0, 1); + inputLayout->addWidget(new QLabel("Y:", this), 0, 2); + m_inputEyeY = createSpinBox(); + inputLayout->addWidget(m_inputEyeY, 0, 3); + inputLayout->addWidget(new QLabel("Z:", this), 0, 4); + m_inputEyeZ = createSpinBox(); + inputLayout->addWidget(m_inputEyeZ, 0, 5); + + // 第2行:Robot X/Y/Z + inputLayout->addWidget(new QLabel("Robot X:", this), 1, 0); + m_inputRobotX = createSpinBox(); + inputLayout->addWidget(m_inputRobotX, 1, 1); + inputLayout->addWidget(new QLabel("Y:", this), 1, 2); + m_inputRobotY = createSpinBox(); + inputLayout->addWidget(m_inputRobotY, 1, 3); + inputLayout->addWidget(new QLabel("Z:", this), 1, 4); + m_inputRobotZ = createSpinBox(); + inputLayout->addWidget(m_inputRobotZ, 1, 5); + + // 第3行:添加到表格按钮 + m_btnEyeToHandAddInput = new QPushButton("添加到表格", this); + connect(m_btnEyeToHandAddInput, &QPushButton::clicked, this, [this]() { + int row = m_tableEyeToHand->rowCount(); + m_tableEyeToHand->insertRow(row); + m_tableEyeToHand->setItem(row, 0, new QTableWidgetItem(QString::number(m_inputEyeX->value(), 'f', 3))); + m_tableEyeToHand->setItem(row, 1, new QTableWidgetItem(QString::number(m_inputEyeY->value(), 'f', 3))); + m_tableEyeToHand->setItem(row, 2, new QTableWidgetItem(QString::number(m_inputEyeZ->value(), 'f', 3))); + m_tableEyeToHand->setItem(row, 3, new QTableWidgetItem(QString::number(m_inputRobotX->value(), 'f', 3))); + m_tableEyeToHand->setItem(row, 4, new QTableWidgetItem(QString::number(m_inputRobotY->value(), 'f', 3))); + m_tableEyeToHand->setItem(row, 5, new QTableWidgetItem(QString::number(m_inputRobotZ->value(), 'f', 3))); + m_tableEyeToHand->scrollToBottom(); + }); + inputLayout->addWidget(m_btnEyeToHandAddInput, 2, 0, 1, 6); + + layout->addWidget(inputGroup); + + return group; +} + +QWidget* CalibDataWidget::createEyeInHandGroup() +{ + QWidget* group = new QWidget(this); + QVBoxLayout* layout = new QVBoxLayout(group); + layout->setContentsMargins(0, 0, 0, 0); + + m_tableEyeInHand = new QTableWidget(this); + m_tableEyeInHand->setColumnCount(9); + m_tableEyeInHand->setHorizontalHeaderLabels({ + "End X", "End Y", "End Z", "End Roll", "End Pitch", "End Yaw", + "Cam X", "Cam Y", "Cam Z" + }); + m_tableEyeInHand->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + m_tableEyeInHand->setSelectionBehavior(QAbstractItemView::SelectRows); + + layout->addWidget(m_tableEyeInHand, 1); + + // 内联按钮行 + QHBoxLayout* btnLayout = new QHBoxLayout(); + m_btnEyeInHandAddRow = new QPushButton("添加行", this); + m_btnEyeInHandDeleteRow = new QPushButton("删除行", this); + m_btnEyeInHandCalib = new QPushButton("Eye-In-Hand 标定", this); + + connect(m_btnEyeInHandAddRow, &QPushButton::clicked, this, [this]() { + int row = m_tableEyeInHand->rowCount(); + m_tableEyeInHand->insertRow(row); + for (int col = 0; col < 9; ++col) { + m_tableEyeInHand->setItem(row, col, new QTableWidgetItem("0")); + } + m_tableEyeInHand->scrollToBottom(); + }); + connect(m_btnEyeInHandDeleteRow, &QPushButton::clicked, this, [this]() { + int row = m_tableEyeInHand->currentRow(); + if (row >= 0) { + m_tableEyeInHand->removeRow(row); + } + }); + connect(m_btnEyeInHandCalib, &QPushButton::clicked, this, &CalibDataWidget::requestEyeInHandCalib); + + btnLayout->addWidget(m_btnEyeInHandAddRow); + btnLayout->addWidget(m_btnEyeInHandDeleteRow); + btnLayout->addWidget(m_btnEyeInHandCalib); + btnLayout->addStretch(); + layout->addLayout(btnLayout); + + // 数据输入区 + QGroupBox* inputGroup = new QGroupBox("数据输入", group); + QGridLayout* inputLayout = new QGridLayout(inputGroup); + inputLayout->setHorizontalSpacing(2); + inputLayout->setVerticalSpacing(4); + inputLayout->setContentsMargins(4, 4, 4, 4); + inputLayout->setColumnStretch(0, 0); + inputLayout->setColumnStretch(1, 1); + inputLayout->setColumnStretch(2, 0); + inputLayout->setColumnStretch(3, 1); + inputLayout->setColumnStretch(4, 0); + inputLayout->setColumnStretch(5, 1); + + auto createSpinBox = [this]() { + QDoubleSpinBox* sb = new QDoubleSpinBox(this); + sb->setRange(-10000, 10000); + sb->setDecimals(3); + return sb; + }; + + // 第1行:End X/Y/Z + inputLayout->addWidget(new QLabel("End X:", this), 0, 0); + m_inputEndX = createSpinBox(); + inputLayout->addWidget(m_inputEndX, 0, 1); + inputLayout->addWidget(new QLabel("Y:", this), 0, 2); + m_inputEndY = createSpinBox(); + inputLayout->addWidget(m_inputEndY, 0, 3); + inputLayout->addWidget(new QLabel("Z:", this), 0, 4); + m_inputEndZ = createSpinBox(); + inputLayout->addWidget(m_inputEndZ, 0, 5); + + // 第2行:End Roll/Pitch/Yaw + inputLayout->addWidget(new QLabel("Roll\302\260:", this), 1, 0); + m_inputEndRoll = createSpinBox(); + inputLayout->addWidget(m_inputEndRoll, 1, 1); + inputLayout->addWidget(new QLabel("Pitch\302\260:", this), 1, 2); + m_inputEndPitch = createSpinBox(); + inputLayout->addWidget(m_inputEndPitch, 1, 3); + inputLayout->addWidget(new QLabel("Yaw\302\260:", this), 1, 4); + m_inputEndYaw = createSpinBox(); + inputLayout->addWidget(m_inputEndYaw, 1, 5); + + // 第3行:Cam X/Y/Z + inputLayout->addWidget(new QLabel("Cam X:", this), 2, 0); + m_inputCamX = createSpinBox(); + inputLayout->addWidget(m_inputCamX, 2, 1); + inputLayout->addWidget(new QLabel("Y:", this), 2, 2); + m_inputCamY = createSpinBox(); + inputLayout->addWidget(m_inputCamY, 2, 3); + inputLayout->addWidget(new QLabel("Z:", this), 2, 4); + m_inputCamZ = createSpinBox(); + inputLayout->addWidget(m_inputCamZ, 2, 5); + + // 第4行:添加到表格按钮 + m_btnEyeInHandAddInput = new QPushButton("添加到表格", this); + connect(m_btnEyeInHandAddInput, &QPushButton::clicked, this, [this]() { + int row = m_tableEyeInHand->rowCount(); + m_tableEyeInHand->insertRow(row); + m_tableEyeInHand->setItem(row, 0, new QTableWidgetItem(QString::number(m_inputEndX->value(), 'f', 3))); + m_tableEyeInHand->setItem(row, 1, new QTableWidgetItem(QString::number(m_inputEndY->value(), 'f', 3))); + m_tableEyeInHand->setItem(row, 2, new QTableWidgetItem(QString::number(m_inputEndZ->value(), 'f', 3))); + m_tableEyeInHand->setItem(row, 3, new QTableWidgetItem(QString::number(m_inputEndRoll->value(), 'f', 3))); + m_tableEyeInHand->setItem(row, 4, new QTableWidgetItem(QString::number(m_inputEndPitch->value(), 'f', 3))); + m_tableEyeInHand->setItem(row, 5, new QTableWidgetItem(QString::number(m_inputEndYaw->value(), 'f', 3))); + m_tableEyeInHand->setItem(row, 6, new QTableWidgetItem(QString::number(m_inputCamX->value(), 'f', 3))); + m_tableEyeInHand->setItem(row, 7, new QTableWidgetItem(QString::number(m_inputCamY->value(), 'f', 3))); + m_tableEyeInHand->setItem(row, 8, new QTableWidgetItem(QString::number(m_inputCamZ->value(), 'f', 3))); + m_tableEyeInHand->scrollToBottom(); + }); + inputLayout->addWidget(m_btnEyeInHandAddInput, 3, 0, 1, 6); + + layout->addWidget(inputGroup); + + return group; +} + +void CalibDataWidget::updateTableVisibility() +{ + int index = m_cbCalibType->currentIndex(); + m_groupEyeToHand->setVisible(index == 0); + m_groupEyeInHand->setVisible(index == 1); + m_groupTCPCalib->setVisible(index == 2); +} + +void CalibDataWidget::onCalibTypeChanged(int index) +{ + updateTableVisibility(); + + // Eye-To-Hand 按钮 + m_btnEyeToHandCalib->setEnabled(index == 0); + m_btnEyeToHandAddRow->setEnabled(index == 0); + m_btnEyeToHandDeleteRow->setEnabled(index == 0); + + // Eye-In-Hand 按钮 + m_btnEyeInHandCalib->setEnabled(index == 1); + m_btnEyeInHandAddRow->setEnabled(index == 1); + m_btnEyeInHandDeleteRow->setEnabled(index == 1); + + // TCP 按钮 + m_tcpAddRowBtn->setEnabled(index == 2); + m_tcpRemoveRowBtn->setEnabled(index == 2); + m_btnTCPCalib->setEnabled(index == 2); + + if (index <= 1) { + emit calibTypeChanged(index == 0 ? HECCalibrationType::EyeToHand : HECCalibrationType::EyeInHand); + } +} + +void CalibDataWidget::getEyeToHandData(std::vector& eyePoints, + std::vector& robotPoints) const +{ + eyePoints.clear(); + robotPoints.clear(); + + for (int row = 0; row < m_tableEyeToHand->rowCount(); ++row) { + HECPoint3D eyePt, robotPt; + + QTableWidgetItem* item0 = m_tableEyeToHand->item(row, 0); + QTableWidgetItem* item1 = m_tableEyeToHand->item(row, 1); + QTableWidgetItem* item2 = m_tableEyeToHand->item(row, 2); + QTableWidgetItem* item3 = m_tableEyeToHand->item(row, 3); + QTableWidgetItem* item4 = m_tableEyeToHand->item(row, 4); + QTableWidgetItem* item5 = m_tableEyeToHand->item(row, 5); + + if (item0 && item1 && item2 && item3 && item4 && item5) { + eyePt.x = item0->text().toDouble(); + eyePt.y = item1->text().toDouble(); + eyePt.z = item2->text().toDouble(); + robotPt.x = item3->text().toDouble(); + robotPt.y = item4->text().toDouble(); + robotPt.z = item5->text().toDouble(); + + eyePoints.push_back(eyePt); + robotPoints.push_back(robotPt); + } + } +} + +void CalibDataWidget::getEyeInHandData(std::vector& calibData) const +{ + calibData.clear(); + + const double deg2rad = M_PI / 180.0; + + for (int row = 0; row < m_tableEyeInHand->rowCount(); ++row) { + HECEyeInHandData data; + + // 获取末端位姿 + double endX = m_tableEyeInHand->item(row, 0) ? + m_tableEyeInHand->item(row, 0)->text().toDouble() : 0; + double endY = m_tableEyeInHand->item(row, 1) ? + m_tableEyeInHand->item(row, 1)->text().toDouble() : 0; + double endZ = m_tableEyeInHand->item(row, 2) ? + m_tableEyeInHand->item(row, 2)->text().toDouble() : 0; + double endRoll = m_tableEyeInHand->item(row, 3) ? + m_tableEyeInHand->item(row, 3)->text().toDouble() * deg2rad : 0; + double endPitch = m_tableEyeInHand->item(row, 4) ? + m_tableEyeInHand->item(row, 4)->text().toDouble() * deg2rad : 0; + double endYaw = m_tableEyeInHand->item(row, 5) ? + m_tableEyeInHand->item(row, 5)->text().toDouble() * deg2rad : 0; + + // 构建末端位姿矩阵 + // 外旋 ZYX: R = Rx(roll) * Ry(pitch) * Rz(yaw) + HECRotationMatrix R; + double cr = cos(endRoll), sr = sin(endRoll); + double cp = cos(endPitch), sp = sin(endPitch); + double cy = cos(endYaw), sy = sin(endYaw); + + R.at(0, 0) = cp * cy; + R.at(0, 1) = -cp * sy; + R.at(0, 2) = sp; + R.at(1, 0) = sr * sp * cy + cr * sy; + R.at(1, 1) = -sr * sp * sy + cr * cy; + R.at(1, 2) = -sr * cp; + R.at(2, 0) = -cr * sp * cy + sr * sy; + R.at(2, 1) = cr * sp * sy + sr * cy; + R.at(2, 2) = cr * cp; + + HECTranslationVector T(endX, endY, endZ); + data.endPose = HECHomogeneousMatrix(R, T); + + // 获取相机观测点 + data.targetInCamera.x = m_tableEyeInHand->item(row, 6) ? + m_tableEyeInHand->item(row, 6)->text().toDouble() : 0; + data.targetInCamera.y = m_tableEyeInHand->item(row, 7) ? + m_tableEyeInHand->item(row, 7)->text().toDouble() : 0; + data.targetInCamera.z = m_tableEyeInHand->item(row, 8) ? + m_tableEyeInHand->item(row, 8)->text().toDouble() : 0; + + calibData.push_back(data); + } +} + +HECCalibrationType CalibDataWidget::getCalibType() const +{ + return m_cbCalibType->currentIndex() == 0 ? + HECCalibrationType::EyeToHand : HECCalibrationType::EyeInHand; +} + +int CalibDataWidget::getCalibTypeIndex() const +{ + return m_cbCalibType->currentIndex(); +} + +void CalibDataWidget::clearAll() +{ + m_tableEyeToHand->setRowCount(0); + m_tableEyeInHand->setRowCount(0); + m_tableTCP->setRowCount(0); +} + +QWidget* CalibDataWidget::createTCPCalibGroup() +{ + QWidget* group = new QWidget(this); + QVBoxLayout* layout = new QVBoxLayout(group); + layout->setContentsMargins(0, 0, 0, 0); + + // 模式选择行 + QHBoxLayout* modeLayout = new QHBoxLayout(); + modeLayout->addWidget(new QLabel("标定模式:", this)); + m_tcpModeCombo = new QComboBox(this); + m_tcpModeCombo->addItem("位置标定 (3-DOF)"); + m_tcpModeCombo->addItem("完整标定 (6-DOF)"); + connect(m_tcpModeCombo, QOverload::of(&QComboBox::currentIndexChanged), + this, &CalibDataWidget::onTCPModeChanged); + modeLayout->addWidget(m_tcpModeCombo); + + modeLayout->addWidget(new QLabel("欧拉角顺序:", this)); + m_tcpEulerOrderCombo = new QComboBox(this); + m_tcpEulerOrderCombo->addItem("XYZ", static_cast(HECEulerOrder::XYZ)); + m_tcpEulerOrderCombo->addItem("XZY", static_cast(HECEulerOrder::XZY)); + m_tcpEulerOrderCombo->addItem("YXZ", static_cast(HECEulerOrder::YXZ)); + m_tcpEulerOrderCombo->addItem("YZX", static_cast(HECEulerOrder::YZX)); + m_tcpEulerOrderCombo->addItem("ZXY", static_cast(HECEulerOrder::ZXY)); + m_tcpEulerOrderCombo->addItem("ZYX (常用)", static_cast(HECEulerOrder::ZYX)); + m_tcpEulerOrderCombo->setCurrentIndex(5); + modeLayout->addWidget(m_tcpEulerOrderCombo); + modeLayout->addStretch(); + layout->addLayout(modeLayout); + + // 法兰位姿表格 + m_tableTCP = new QTableWidget(this); + m_tableTCP->setColumnCount(6); + m_tableTCP->setHorizontalHeaderLabels({"X", "Y", "Z", "Roll(°)", "Pitch(°)", "Yaw(°)"}); + m_tableTCP->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + m_tableTCP->setSelectionBehavior(QAbstractItemView::SelectRows); + layout->addWidget(m_tableTCP, 1); + + // 按钮行(含 TCP 标定按钮) + QHBoxLayout* btnLayout = new QHBoxLayout(); + m_tcpAddRowBtn = new QPushButton("添加行", this); + m_tcpRemoveRowBtn = new QPushButton("删除行", this); + m_btnTCPCalib = new QPushButton("TCP 标定", this); + connect(m_tcpAddRowBtn, &QPushButton::clicked, this, &CalibDataWidget::onTCPAddRow); + connect(m_tcpRemoveRowBtn, &QPushButton::clicked, this, &CalibDataWidget::onTCPRemoveRow); + connect(m_btnTCPCalib, &QPushButton::clicked, this, &CalibDataWidget::requestTCPCalib); + btnLayout->addWidget(m_tcpAddRowBtn); + btnLayout->addWidget(m_tcpRemoveRowBtn); + btnLayout->addWidget(m_btnTCPCalib); + btnLayout->addStretch(); + layout->addLayout(btnLayout); + + // 数据输入区 + QGroupBox* inputGroup = new QGroupBox("数据输入", group); + QGridLayout* inputLayout = new QGridLayout(inputGroup); + inputLayout->setHorizontalSpacing(2); + inputLayout->setVerticalSpacing(4); + inputLayout->setContentsMargins(4, 4, 4, 4); + inputLayout->setColumnStretch(0, 0); + inputLayout->setColumnStretch(1, 1); + inputLayout->setColumnStretch(2, 0); + inputLayout->setColumnStretch(3, 1); + inputLayout->setColumnStretch(4, 0); + inputLayout->setColumnStretch(5, 1); + + auto createSpinBox = [this]() { + QDoubleSpinBox* sb = new QDoubleSpinBox(this); + sb->setRange(-10000, 10000); + sb->setDecimals(3); + return sb; + }; + + // 第1行:X/Y/Z + inputLayout->addWidget(new QLabel("X:", this), 0, 0); + m_inputTcpX = createSpinBox(); + inputLayout->addWidget(m_inputTcpX, 0, 1); + inputLayout->addWidget(new QLabel("Y:", this), 0, 2); + m_inputTcpY = createSpinBox(); + inputLayout->addWidget(m_inputTcpY, 0, 3); + inputLayout->addWidget(new QLabel("Z:", this), 0, 4); + m_inputTcpZ = createSpinBox(); + inputLayout->addWidget(m_inputTcpZ, 0, 5); + + // 第2行:Roll/Pitch/Yaw + inputLayout->addWidget(new QLabel("Roll\302\260:", this), 1, 0); + m_inputTcpRx = createSpinBox(); + inputLayout->addWidget(m_inputTcpRx, 1, 1); + inputLayout->addWidget(new QLabel("Pitch\302\260:", this), 1, 2); + m_inputTcpRy = createSpinBox(); + inputLayout->addWidget(m_inputTcpRy, 1, 3); + inputLayout->addWidget(new QLabel("Yaw\302\260:", this), 1, 4); + m_inputTcpRz = createSpinBox(); + inputLayout->addWidget(m_inputTcpRz, 1, 5); + + // 第3行:添加到表格按钮 + m_btnTcpAddInput = new QPushButton("添加到表格", this); + connect(m_btnTcpAddInput, &QPushButton::clicked, this, [this]() { + int row = m_tableTCP->rowCount(); + m_tableTCP->insertRow(row); + m_tableTCP->setItem(row, 0, new QTableWidgetItem(QString::number(m_inputTcpX->value(), 'f', 3))); + m_tableTCP->setItem(row, 1, new QTableWidgetItem(QString::number(m_inputTcpY->value(), 'f', 3))); + m_tableTCP->setItem(row, 2, new QTableWidgetItem(QString::number(m_inputTcpZ->value(), 'f', 3))); + m_tableTCP->setItem(row, 3, new QTableWidgetItem(QString::number(m_inputTcpRx->value(), 'f', 3))); + m_tableTCP->setItem(row, 4, new QTableWidgetItem(QString::number(m_inputTcpRy->value(), 'f', 3))); + m_tableTCP->setItem(row, 5, new QTableWidgetItem(QString::number(m_inputTcpRz->value(), 'f', 3))); + m_tableTCP->scrollToBottom(); + }); + inputLayout->addWidget(m_btnTcpAddInput, 2, 0, 1, 6); + + layout->addWidget(inputGroup); + + // 6-DOF 姿态参数组(默认隐藏) + m_tcpOrientationGroup = new QGroupBox("6-DOF 姿态参数", this); + QGridLayout* oriLayout = new QGridLayout(m_tcpOrientationGroup); + + oriLayout->addWidget(new QLabel("参考位姿索引:", this), 0, 0); + m_tcpRefPoseIndex = new QSpinBox(this); + m_tcpRefPoseIndex->setRange(0, 999); + m_tcpRefPoseIndex->setValue(0); + oriLayout->addWidget(m_tcpRefPoseIndex, 0, 1); + + oriLayout->addWidget(new QLabel("期望世界朝向 Rx(°):", this), 1, 0); + m_tcpWorldRx = new QDoubleSpinBox(this); + m_tcpWorldRx->setRange(-360, 360); + m_tcpWorldRx->setDecimals(2); + oriLayout->addWidget(m_tcpWorldRx, 1, 1); + + oriLayout->addWidget(new QLabel("Ry(°):", this), 1, 2); + m_tcpWorldRy = new QDoubleSpinBox(this); + m_tcpWorldRy->setRange(-360, 360); + m_tcpWorldRy->setDecimals(2); + oriLayout->addWidget(m_tcpWorldRy, 1, 3); + + oriLayout->addWidget(new QLabel("Rz(°):", this), 1, 4); + m_tcpWorldRz = new QDoubleSpinBox(this); + m_tcpWorldRz->setRange(-360, 360); + m_tcpWorldRz->setDecimals(2); + oriLayout->addWidget(m_tcpWorldRz, 1, 5); + + m_tcpOrientationGroup->setVisible(false); + layout->addWidget(m_tcpOrientationGroup); + + return group; +} + +void CalibDataWidget::onTCPModeChanged(int index) +{ + m_tcpOrientationGroup->setVisible(index == 1); +} + +void CalibDataWidget::onTCPAddRow() +{ + int row = m_tableTCP->rowCount(); + m_tableTCP->insertRow(row); + for (int col = 0; col < 6; ++col) { + QTableWidgetItem* item = new QTableWidgetItem("0"); + m_tableTCP->setItem(row, col, item); + } + m_tableTCP->scrollToBottom(); +} + +void CalibDataWidget::onTCPRemoveRow() +{ + int row = m_tableTCP->currentRow(); + if (row >= 0) { + m_tableTCP->removeRow(row); + } +} + +HECTCPCalibData CalibDataWidget::getTCPCalibData() const +{ + HECTCPCalibData data; + + // 标定模式 + data.mode = (m_tcpModeCombo->currentIndex() == 0) ? + HECTCPCalibMode::PositionOnly : HECTCPCalibMode::Full6DOF; + + // 欧拉角顺序 + HECEulerOrder eulerOrder = static_cast( + m_tcpEulerOrderCombo->currentData().toInt()); + + // 读取表格中的法兰位姿 + for (int row = 0; row < m_tableTCP->rowCount(); ++row) { + HECTCPCalibPose pose; + pose.x = m_tableTCP->item(row, 0) ? m_tableTCP->item(row, 0)->text().toDouble() : 0; + pose.y = m_tableTCP->item(row, 1) ? m_tableTCP->item(row, 1)->text().toDouble() : 0; + pose.z = m_tableTCP->item(row, 2) ? m_tableTCP->item(row, 2)->text().toDouble() : 0; + pose.rx = m_tableTCP->item(row, 3) ? m_tableTCP->item(row, 3)->text().toDouble() : 0; + pose.ry = m_tableTCP->item(row, 4) ? m_tableTCP->item(row, 4)->text().toDouble() : 0; + pose.rz = m_tableTCP->item(row, 5) ? m_tableTCP->item(row, 5)->text().toDouble() : 0; + pose.eulerOrder = eulerOrder; + data.poses.push_back(pose); + } + + // 6-DOF 参数 + data.referencePoseIndex = m_tcpRefPoseIndex->value(); + data.worldRx = m_tcpWorldRx->value(); + data.worldRy = m_tcpWorldRy->value(); + data.worldRz = m_tcpWorldRz->value(); + data.worldEulerOrder = eulerOrder; + + return data; +} + +void CalibDataWidget::setRobotInput(double x, double y, double z, + double rx, double ry, double rz) +{ + int mode = m_cbCalibType->currentIndex(); + if (mode == 0) { + // Eye-To-Hand: 仅填充 Robot X/Y/Z + m_inputRobotX->setValue(x); + m_inputRobotY->setValue(y); + m_inputRobotZ->setValue(z); + } else if (mode == 1) { + // Eye-In-Hand: 填充末端位姿 + m_inputEndX->setValue(x); + m_inputEndY->setValue(y); + m_inputEndZ->setValue(z); + m_inputEndRoll->setValue(rx); + m_inputEndPitch->setValue(ry); + m_inputEndYaw->setValue(rz); + } else if (mode == 2) { + // TCP: 填充位姿 + m_inputTcpX->setValue(x); + m_inputTcpY->setValue(y); + m_inputTcpZ->setValue(z); + m_inputTcpRx->setValue(rx); + m_inputTcpRy->setValue(ry); + m_inputTcpRz->setValue(rz); + } +} + +void CalibDataWidget::setCameraInput(double x, double y, double z, + double rx, double ry, double rz) +{ + int mode = m_cbCalibType->currentIndex(); + if (mode == 0) { + // Eye-To-Hand: 填充相机坐标 Eye X/Y/Z + m_inputEyeX->setValue(x); + m_inputEyeY->setValue(y); + m_inputEyeZ->setValue(z); + } else if (mode == 1) { + // Eye-In-Hand: 填充相机观测点 + m_inputCamX->setValue(x); + m_inputCamY->setValue(y); + m_inputCamZ->setValue(z); + } + // TCP 模式不需要相机输入 +} + +void CalibDataWidget::saveCalibData(const QString& filePath) const +{ + QSettings ini(filePath, QSettings::IniFormat); + ini.setIniCodec("UTF-8"); + + int mode = m_cbCalibType->currentIndex(); + + // [CommInfo] 通用信息 + ini.beginGroup("CommInfo"); + ini.setValue("eCalibType", mode); // 0=EyeToHand, 1=EyeInHand, 2=TCP + ini.setValue("sCalibTime", QDateTime::currentDateTime().toString("yyyy-MM-dd-HH-mm-ss")); + + if (mode == 0) { + int count = m_tableEyeToHand->rowCount(); + ini.setValue("nPointCount", count); + ini.endGroup(); + + for (int row = 0; row < count; ++row) { + ini.beginGroup(QString("CalibPointInfo_%1").arg(row)); + ini.setValue("dEyeX", m_tableEyeToHand->item(row, 0) ? m_tableEyeToHand->item(row, 0)->text() : "0"); + ini.setValue("dEyeY", m_tableEyeToHand->item(row, 1) ? m_tableEyeToHand->item(row, 1)->text() : "0"); + ini.setValue("dEyeZ", m_tableEyeToHand->item(row, 2) ? m_tableEyeToHand->item(row, 2)->text() : "0"); + ini.setValue("dRobotX", m_tableEyeToHand->item(row, 3) ? m_tableEyeToHand->item(row, 3)->text() : "0"); + ini.setValue("dRobotY", m_tableEyeToHand->item(row, 4) ? m_tableEyeToHand->item(row, 4)->text() : "0"); + ini.setValue("dRobotZ", m_tableEyeToHand->item(row, 5) ? m_tableEyeToHand->item(row, 5)->text() : "0"); + ini.endGroup(); + } + } else if (mode == 1) { + int count = m_tableEyeInHand->rowCount(); + ini.setValue("nPointCount", count); + ini.endGroup(); + + for (int row = 0; row < count; ++row) { + ini.beginGroup(QString("CalibPointInfo_%1").arg(row)); + ini.setValue("dEndX", m_tableEyeInHand->item(row, 0) ? m_tableEyeInHand->item(row, 0)->text() : "0"); + ini.setValue("dEndY", m_tableEyeInHand->item(row, 1) ? m_tableEyeInHand->item(row, 1)->text() : "0"); + ini.setValue("dEndZ", m_tableEyeInHand->item(row, 2) ? m_tableEyeInHand->item(row, 2)->text() : "0"); + ini.setValue("dEndRoll", m_tableEyeInHand->item(row, 3) ? m_tableEyeInHand->item(row, 3)->text() : "0"); + ini.setValue("dEndPitch", m_tableEyeInHand->item(row, 4) ? m_tableEyeInHand->item(row, 4)->text() : "0"); + ini.setValue("dEndYaw", m_tableEyeInHand->item(row, 5) ? m_tableEyeInHand->item(row, 5)->text() : "0"); + ini.setValue("dCamX", m_tableEyeInHand->item(row, 6) ? m_tableEyeInHand->item(row, 6)->text() : "0"); + ini.setValue("dCamY", m_tableEyeInHand->item(row, 7) ? m_tableEyeInHand->item(row, 7)->text() : "0"); + ini.setValue("dCamZ", m_tableEyeInHand->item(row, 8) ? m_tableEyeInHand->item(row, 8)->text() : "0"); + ini.endGroup(); + } + } else { + int count = m_tableTCP->rowCount(); + ini.setValue("nPoseCount", count); + ini.setValue("eCalibMode", m_tcpModeCombo->currentIndex()); + ini.setValue("eEulerOrder", m_tcpEulerOrderCombo->currentIndex()); + ini.setValue("nRefPoseIndex", m_tcpRefPoseIndex->value()); + ini.setValue("dWorldRx", m_tcpWorldRx->value()); + ini.setValue("dWorldRy", m_tcpWorldRy->value()); + ini.setValue("dWorldRz", m_tcpWorldRz->value()); + ini.endGroup(); + + for (int row = 0; row < count; ++row) { + ini.beginGroup(QString("CalibPoseInfo_%1").arg(row)); + ini.setValue("dX", m_tableTCP->item(row, 0) ? m_tableTCP->item(row, 0)->text() : "0"); + ini.setValue("dY", m_tableTCP->item(row, 1) ? m_tableTCP->item(row, 1)->text() : "0"); + ini.setValue("dZ", m_tableTCP->item(row, 2) ? m_tableTCP->item(row, 2)->text() : "0"); + ini.setValue("dRx", m_tableTCP->item(row, 3) ? m_tableTCP->item(row, 3)->text() : "0"); + ini.setValue("dRy", m_tableTCP->item(row, 4) ? m_tableTCP->item(row, 4)->text() : "0"); + ini.setValue("dRz", m_tableTCP->item(row, 5) ? m_tableTCP->item(row, 5)->text() : "0"); + ini.endGroup(); + } + } +} + +void CalibDataWidget::loadCalibData(const QString& filePath) +{ + QSettings ini(filePath, QSettings::IniFormat); + ini.setIniCodec("UTF-8"); + + ini.beginGroup("CommInfo"); + int calibType = ini.value("eCalibType", -1).toInt(); + + if (calibType == 0) { + // Eye-To-Hand + int count = ini.value("nPointCount", 0).toInt(); + ini.endGroup(); + + m_cbCalibType->setCurrentIndex(0); + m_tableEyeToHand->setRowCount(0); + for (int i = 0; i < count; ++i) { + ini.beginGroup(QString("CalibPointInfo_%1").arg(i)); + int row = m_tableEyeToHand->rowCount(); + m_tableEyeToHand->insertRow(row); + m_tableEyeToHand->setItem(row, 0, new QTableWidgetItem(ini.value("dEyeX", "0").toString())); + m_tableEyeToHand->setItem(row, 1, new QTableWidgetItem(ini.value("dEyeY", "0").toString())); + m_tableEyeToHand->setItem(row, 2, new QTableWidgetItem(ini.value("dEyeZ", "0").toString())); + m_tableEyeToHand->setItem(row, 3, new QTableWidgetItem(ini.value("dRobotX", "0").toString())); + m_tableEyeToHand->setItem(row, 4, new QTableWidgetItem(ini.value("dRobotY", "0").toString())); + m_tableEyeToHand->setItem(row, 5, new QTableWidgetItem(ini.value("dRobotZ", "0").toString())); + ini.endGroup(); + } + } else if (calibType == 1) { + // Eye-In-Hand + int count = ini.value("nPointCount", 0).toInt(); + ini.endGroup(); + + m_cbCalibType->setCurrentIndex(1); + m_tableEyeInHand->setRowCount(0); + for (int i = 0; i < count; ++i) { + ini.beginGroup(QString("CalibPointInfo_%1").arg(i)); + int row = m_tableEyeInHand->rowCount(); + m_tableEyeInHand->insertRow(row); + m_tableEyeInHand->setItem(row, 0, new QTableWidgetItem(ini.value("dEndX", "0").toString())); + m_tableEyeInHand->setItem(row, 1, new QTableWidgetItem(ini.value("dEndY", "0").toString())); + m_tableEyeInHand->setItem(row, 2, new QTableWidgetItem(ini.value("dEndZ", "0").toString())); + m_tableEyeInHand->setItem(row, 3, new QTableWidgetItem(ini.value("dEndRoll", "0").toString())); + m_tableEyeInHand->setItem(row, 4, new QTableWidgetItem(ini.value("dEndPitch", "0").toString())); + m_tableEyeInHand->setItem(row, 5, new QTableWidgetItem(ini.value("dEndYaw", "0").toString())); + m_tableEyeInHand->setItem(row, 6, new QTableWidgetItem(ini.value("dCamX", "0").toString())); + m_tableEyeInHand->setItem(row, 7, new QTableWidgetItem(ini.value("dCamY", "0").toString())); + m_tableEyeInHand->setItem(row, 8, new QTableWidgetItem(ini.value("dCamZ", "0").toString())); + ini.endGroup(); + } + } else if (calibType == 2) { + // TCP + int count = ini.value("nPoseCount", 0).toInt(); + m_tcpModeCombo->setCurrentIndex(ini.value("eCalibMode", 0).toInt()); + m_tcpEulerOrderCombo->setCurrentIndex(ini.value("eEulerOrder", 5).toInt()); + m_tcpRefPoseIndex->setValue(ini.value("nRefPoseIndex", 0).toInt()); + m_tcpWorldRx->setValue(ini.value("dWorldRx", 0).toDouble()); + m_tcpWorldRy->setValue(ini.value("dWorldRy", 0).toDouble()); + m_tcpWorldRz->setValue(ini.value("dWorldRz", 0).toDouble()); + ini.endGroup(); + + m_cbCalibType->setCurrentIndex(2); + m_tableTCP->setRowCount(0); + for (int i = 0; i < count; ++i) { + ini.beginGroup(QString("CalibPoseInfo_%1").arg(i)); + int row = m_tableTCP->rowCount(); + m_tableTCP->insertRow(row); + m_tableTCP->setItem(row, 0, new QTableWidgetItem(ini.value("dX", "0").toString())); + m_tableTCP->setItem(row, 1, new QTableWidgetItem(ini.value("dY", "0").toString())); + m_tableTCP->setItem(row, 2, new QTableWidgetItem(ini.value("dZ", "0").toString())); + m_tableTCP->setItem(row, 3, new QTableWidgetItem(ini.value("dRx", "0").toString())); + m_tableTCP->setItem(row, 4, new QTableWidgetItem(ini.value("dRy", "0").toString())); + m_tableTCP->setItem(row, 5, new QTableWidgetItem(ini.value("dRz", "0").toString())); + ini.endGroup(); + } + } else { + ini.endGroup(); + } +} diff --git a/Tools/CalibView/Src/CalibViewMainWindow.cpp b/Tools/CalibView/Src/CalibViewMainWindow.cpp index 6fa6628..8aecc40 100644 --- a/Tools/CalibView/Src/CalibViewMainWindow.cpp +++ b/Tools/CalibView/Src/CalibViewMainWindow.cpp @@ -1,676 +1,741 @@ -#include "CalibViewMainWindow.h" -#include "CalibDataWidget.h" -#include "CalibResultWidget.h" -#include "MainWindow.h" -#include "VrEyeViewWidget.h" -#include "../../SpinBoxPasteHelper.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -CalibViewMainWindow::CalibViewMainWindow(QWidget* parent) - : QMainWindow(parent) - , m_calib(nullptr) - , m_dataWidget(nullptr) - , m_resultWidget(nullptr) - , m_sbTransformX(nullptr) - , m_sbTransformY(nullptr) - , m_sbTransformZ(nullptr) - , m_btnTransform(nullptr) - , m_sbRoll(nullptr) - , m_sbPitch(nullptr) - , m_sbYaw(nullptr) - , m_cbEulerOrder(nullptr) - , m_btnEulerConvert(nullptr) - , m_logEdit(nullptr) - , m_hasResult(false) - , m_robotView(nullptr) - , m_vrEyeView(nullptr) -{ - // 创建标定实例 - m_calib = CreateHandEyeCalibInstance(); - - setupUI(); - createMenuBar(); - SpinBoxPasteHelper::install(this); - - setWindowTitle("CalibView - 手眼标定测试工具"); - resize(1000, 600); - - updateStatusBar("就绪"); -} - -CalibViewMainWindow::~CalibViewMainWindow() -{ - if (m_calib) { - DestroyHandEyeCalibInstance(m_calib); - m_calib = nullptr; - } -} - -QWidget* CalibViewMainWindow::createRightPanel() -{ - QWidget* rightPanel = new QWidget(this); - QVBoxLayout* rightLayout = new QVBoxLayout(rightPanel); - - // 坐标变换测试组 - QGroupBox* transformGroup = new QGroupBox("坐标变换测试", this); - QGridLayout* transformLayout = new QGridLayout(transformGroup); - - transformLayout->addWidget(new QLabel("X:", this), 0, 0); - m_sbTransformX = new QDoubleSpinBox(this); - m_sbTransformX->setRange(-10000, 10000); - m_sbTransformX->setDecimals(3); - transformLayout->addWidget(m_sbTransformX, 0, 1); - - transformLayout->addWidget(new QLabel("Y:", this), 0, 2); - m_sbTransformY = new QDoubleSpinBox(this); - m_sbTransformY->setRange(-10000, 10000); - m_sbTransformY->setDecimals(3); - transformLayout->addWidget(m_sbTransformY, 0, 3); - - transformLayout->addWidget(new QLabel("Z:", this), 0, 4); - m_sbTransformZ = new QDoubleSpinBox(this); - m_sbTransformZ->setRange(-10000, 10000); - m_sbTransformZ->setDecimals(3); - transformLayout->addWidget(m_sbTransformZ, 0, 5); - - m_btnTransform = new QPushButton("变换", this); - connect(m_btnTransform, &QPushButton::clicked, this, &CalibViewMainWindow::onTransformTest); - transformLayout->addWidget(m_btnTransform, 1, 0, 1, 6); - - rightLayout->addWidget(transformGroup); - - // 欧拉角转换测试组 - QGroupBox* eulerGroup = new QGroupBox("欧拉角转换测试", this); - QGridLayout* eulerLayout = new QGridLayout(eulerGroup); - - eulerLayout->addWidget(new QLabel("Roll (\302\260):", this), 0, 0); - m_sbRoll = new QDoubleSpinBox(this); - m_sbRoll->setRange(-180, 180); - m_sbRoll->setDecimals(2); - eulerLayout->addWidget(m_sbRoll, 0, 1); - - eulerLayout->addWidget(new QLabel("Pitch (\302\260):", this), 0, 2); - m_sbPitch = new QDoubleSpinBox(this); - m_sbPitch->setRange(-180, 180); - m_sbPitch->setDecimals(2); - eulerLayout->addWidget(m_sbPitch, 0, 3); - - eulerLayout->addWidget(new QLabel("Yaw (\302\260):", this), 0, 4); - m_sbYaw = new QDoubleSpinBox(this); - m_sbYaw->setRange(-180, 180); - m_sbYaw->setDecimals(2); - eulerLayout->addWidget(m_sbYaw, 0, 5); - - eulerLayout->addWidget(new QLabel("旋转顺序:", this), 1, 0); - m_cbEulerOrder = new QComboBox(this); - m_cbEulerOrder->addItem("XYZ", static_cast(HECEulerOrder::XYZ)); - m_cbEulerOrder->addItem("XZY", static_cast(HECEulerOrder::XZY)); - m_cbEulerOrder->addItem("YXZ", static_cast(HECEulerOrder::YXZ)); - m_cbEulerOrder->addItem("YZX", static_cast(HECEulerOrder::YZX)); - m_cbEulerOrder->addItem("ZXY", static_cast(HECEulerOrder::ZXY)); - m_cbEulerOrder->addItem("ZYX (常用)", static_cast(HECEulerOrder::ZYX)); - m_cbEulerOrder->setCurrentIndex(5); // 默认 ZYX - eulerLayout->addWidget(m_cbEulerOrder, 1, 1, 1, 2); - - m_btnEulerConvert = new QPushButton("转换", this); - connect(m_btnEulerConvert, &QPushButton::clicked, this, &CalibViewMainWindow::onEulerTest); - eulerLayout->addWidget(m_btnEulerConvert, 1, 3, 1, 3); - - rightLayout->addWidget(eulerGroup); - - // 日志组 - QGroupBox* logGroup = new QGroupBox("日志", this); - QVBoxLayout* logLayout = new QVBoxLayout(logGroup); - - m_logEdit = new QTextEdit(this); - m_logEdit->setReadOnly(true); - m_logEdit->setFont(QFont("Consolas", 9)); - - logLayout->addWidget(m_logEdit); - - rightLayout->addWidget(logGroup, 1); // stretch=1 让日志区占据剩余空间 - - return rightPanel; -} - -void CalibViewMainWindow::setupUI() -{ - // 创建中央控件 - QWidget* centralWidget = new QWidget(this); - QHBoxLayout* mainLayout = new QHBoxLayout(centralWidget); - - // 创建分割器 - QSplitter* splitter = new QSplitter(Qt::Horizontal, this); - - // 左侧面板:数据输入 + 标定结果 - QWidget* leftPanel = new QWidget(this); - QVBoxLayout* leftLayout = new QVBoxLayout(leftPanel); - - // 数据输入(可滚动) - QScrollArea* dataScroll = new QScrollArea(this); - m_dataWidget = new CalibDataWidget(this); - dataScroll->setWidget(m_dataWidget); - dataScroll->setWidgetResizable(true); - leftLayout->addWidget(dataScroll, 1); - - // 标定结果 - m_resultWidget = new CalibResultWidget(this); - leftLayout->addWidget(m_resultWidget); - - splitter->addWidget(leftPanel); - - // 右侧面板:测试工具 + 日志 - QWidget* rightPanel = createRightPanel(); - splitter->addWidget(rightPanel); - - // 设置分割比例 - splitter->setStretchFactor(0, 2); - splitter->setStretchFactor(1, 1); - - mainLayout->addWidget(splitter); - setCentralWidget(centralWidget); - - // 创建状态栏 - statusBar()->showMessage("就绪"); - - // 连接 CalibDataWidget 的信号 - connect(m_dataWidget, &CalibDataWidget::requestEyeToHandCalib, - this, &CalibViewMainWindow::onEyeToHandCalib); - connect(m_dataWidget, &CalibDataWidget::requestEyeInHandCalib, - this, &CalibViewMainWindow::onEyeInHandCalib); - connect(m_dataWidget, &CalibDataWidget::requestTCPCalib, - this, &CalibViewMainWindow::onTCPCalib); -} - -void CalibViewMainWindow::createMenuBar() -{ - // 文件菜单 - QMenu* fileMenu = menuBar()->addMenu("文件(&F)"); - - QAction* actSave = fileMenu->addAction("保存结果(&S)"); - actSave->setShortcut(QKeySequence::Save); - connect(actSave, &QAction::triggered, this, &CalibViewMainWindow::onSaveResult); - - QAction* actLoad = fileMenu->addAction("加载结果(&L)"); - actLoad->setShortcut(QKeySequence::Open); - connect(actLoad, &QAction::triggered, this, &CalibViewMainWindow::onLoadResult); - - fileMenu->addSeparator(); - - QAction* actExit = fileMenu->addAction("退出(&X)"); - actExit->setShortcut(QKeySequence::Quit); - connect(actExit, &QAction::triggered, this, &QMainWindow::close); - - // 标定菜单 - QMenu* calibMenu = menuBar()->addMenu("标定(&C)"); - - QAction* actEyeToHand = calibMenu->addAction("Eye-To-Hand 标定(&E)"); - connect(actEyeToHand, &QAction::triggered, this, &CalibViewMainWindow::onEyeToHandCalib); - - QAction* actEyeInHand = calibMenu->addAction("Eye-In-Hand 标定(&I)"); - connect(actEyeInHand, &QAction::triggered, this, &CalibViewMainWindow::onEyeInHandCalib); - - QAction* actTCPCalib = calibMenu->addAction("TCP 标定(&P)"); - connect(actTCPCalib, &QAction::triggered, this, &CalibViewMainWindow::onTCPCalib); - - calibMenu->addSeparator(); - - QAction* actTransform = calibMenu->addAction("坐标变换测试(&T)"); - connect(actTransform, &QAction::triggered, this, &CalibViewMainWindow::onTransformTest); - - QAction* actEuler = calibMenu->addAction("欧拉角转换测试(&U)"); - connect(actEuler, &QAction::triggered, this, &CalibViewMainWindow::onEulerTest); - - calibMenu->addSeparator(); - - QAction* actClear = calibMenu->addAction("清除所有(&C)"); - connect(actClear, &QAction::triggered, this, &CalibViewMainWindow::onClearAll); - - // 工具菜单 - QMenu* toolMenu = menuBar()->addMenu("工具(&T)"); - QAction* actRobotView = toolMenu->addAction("机器人控制(&R)"); - connect(actRobotView, &QAction::triggered, this, &CalibViewMainWindow::onOpenRobotView); - - QAction* actVrEyeView = toolMenu->addAction("相机标定板检测(&V)"); - connect(actVrEyeView, &QAction::triggered, this, &CalibViewMainWindow::onOpenVrEyeView); - - // 帮助菜单 - QMenu* helpMenu = menuBar()->addMenu("帮助(&H)"); - - QAction* actAbout = helpMenu->addAction("关于(&A)"); - connect(actAbout, &QAction::triggered, this, [this]() { - QMessageBox::about(this, "关于 CalibView", - "CalibView - 手眼标定测试工具\n\n" - "用于测试 HandEyeCalib 模块的各项功能:\n" - "- Eye-To-Hand 标定\n" - "- Eye-In-Hand 标定\n" - "- TCP 标定\n" - "- 坐标变换\n" - "- 欧拉角转换\n\n" - "基于 Eigen 库实现的 SVD 分解算法"); - }); -} - -void CalibViewMainWindow::updateStatusBar(const QString& message) -{ - statusBar()->showMessage(message); -} - -void CalibViewMainWindow::appendLog(const QString& message) -{ - m_logEdit->append(message); -} - -void CalibViewMainWindow::onEyeToHandCalib() -{ - if (!m_calib) { - QMessageBox::critical(this, "错误", "标定实例未初始化"); - return; - } - - std::vector eyePoints; - std::vector robotPoints; - m_dataWidget->getEyeToHandData(eyePoints, robotPoints); - - if (eyePoints.size() < 3) { - QMessageBox::warning(this, "警告", "至少需要3组对应点进行标定"); - return; - } - - appendLog("开始 Eye-To-Hand 标定..."); - appendLog(QString("输入点数: %1").arg(eyePoints.size())); - - int ret = m_calib->CalculateRT(eyePoints, robotPoints, m_currentResult); - - if (ret == 0) { - m_hasResult = true; - m_resultWidget->showCalibResult(m_currentResult); - appendLog(QString("标定成功,误差: %1 mm") - .arg(m_currentResult.error, 0, 'f', 4)); - updateStatusBar("Eye-To-Hand 标定完成"); - emit calibrationCompleted(m_currentResult); - } else { - appendLog(QString("标定失败,错误码: %1").arg(ret)); - QMessageBox::critical(this, "错误", QString("标定失败,错误码: %1").arg(ret)); - } -} - -void CalibViewMainWindow::onEyeInHandCalib() -{ - if (!m_calib) { - QMessageBox::critical(this, "错误", "标定实例未初始化"); - return; - } - - std::vector calibData; - m_dataWidget->getEyeInHandData(calibData); - - if (calibData.size() < 3) { - QMessageBox::warning(this, "警告", "至少需要3组数据进行标定"); - return; - } - - appendLog("开始 Eye-In-Hand 标定..."); - appendLog(QString("输入数据组数: %1").arg(calibData.size())); - - int ret = m_calib->CalculateEyeInHand(calibData, m_currentResult); - - if (ret == 0) { - m_hasResult = true; - m_resultWidget->showCalibResult(m_currentResult); - appendLog(QString("标定成功,误差: %1 mm") - .arg(m_currentResult.error, 0, 'f', 4)); - updateStatusBar("Eye-In-Hand 标定完成"); - emit calibrationCompleted(m_currentResult); - } else { - appendLog(QString("标定失败,错误码: %1").arg(ret)); - QMessageBox::critical(this, "错误", QString("标定失败,错误码: %1").arg(ret)); - } -} - -void CalibViewMainWindow::onTCPCalib() -{ - if (!m_calib) { - QMessageBox::critical(this, "错误", "标定实例未初始化"); - return; - } - - HECTCPCalibData tcpData = m_dataWidget->getTCPCalibData(); - - if (tcpData.poses.size() < 3) { - QMessageBox::warning(this, "警告", "至少需要3组法兰位姿进行TCP标定"); - return; - } - - if (tcpData.mode == HECTCPCalibMode::Full6DOF) { - if (tcpData.referencePoseIndex < 0 || - tcpData.referencePoseIndex >= static_cast(tcpData.poses.size())) { - QMessageBox::warning(this, "警告", - QString("参考位姿索引 %1 越界,有效范围: 0-%2") - .arg(tcpData.referencePoseIndex) - .arg(tcpData.poses.size() - 1)); - return; - } - } - - QString modeName = (tcpData.mode == HECTCPCalibMode::PositionOnly) ? - "3-DOF 位置标定" : "6-DOF 完整标定"; - appendLog(QString("开始 TCP 标定 (%1)...").arg(modeName)); - appendLog(QString("输入位姿数: %1").arg(tcpData.poses.size())); - - HECTCPCalibResult tcpResult = m_calib->CalculateTCP(tcpData); - - if (tcpResult.success) { - m_resultWidget->showTCPCalibResult(tcpResult); - - appendLog("=== TCP 标定结果 ==="); - appendLog(QString("TCP 位置偏移: tx=%1, ty=%2, tz=%3") - .arg(tcpResult.tx, 0, 'f', 3) - .arg(tcpResult.ty, 0, 'f', 3) - .arg(tcpResult.tz, 0, 'f', 3)); - if (tcpResult.rx != 0 || tcpResult.ry != 0 || tcpResult.rz != 0) { - appendLog(QString("TCP 姿态偏移: rx=%1\302\260, ry=%2\302\260, rz=%3\302\260") - .arg(tcpResult.rx, 0, 'f', 2) - .arg(tcpResult.ry, 0, 'f', 2) - .arg(tcpResult.rz, 0, 'f', 2)); - } - appendLog(QString("残差误差: %1 mm").arg(tcpResult.residualError, 0, 'f', 4)); - appendLog("TCP 标定成功"); - updateStatusBar("TCP 标定完成"); - } else { - appendLog(QString("TCP 标定失败: %1") - .arg(QString::fromStdString(tcpResult.errorMessage))); - QMessageBox::critical(this, "错误", - QString("TCP 标定失败: %1").arg(QString::fromStdString(tcpResult.errorMessage))); - } -} - -void CalibViewMainWindow::onTransformTest() -{ - if (!m_calib) { - QMessageBox::critical(this, "错误", "标定实例未初始化"); - return; - } - - if (!m_hasResult) { - QMessageBox::warning(this, "警告", "请先执行标定或加载标定结果"); - return; - } - - HECPoint3D srcPoint( - m_sbTransformX->value(), - m_sbTransformY->value(), - m_sbTransformZ->value() - ); - HECPoint3D dstPoint; - - m_calib->TransformPoint(m_currentResult.R, m_currentResult.T, srcPoint, dstPoint); - - appendLog(QString("坐标变换结果:")); - appendLog(QString(" 源点: (%1, %2, %3)") - .arg(srcPoint.x, 0, 'f', 3) - .arg(srcPoint.y, 0, 'f', 3) - .arg(srcPoint.z, 0, 'f', 3)); - appendLog(QString(" 目标点: (%1, %2, %3)") - .arg(dstPoint.x, 0, 'f', 3) - .arg(dstPoint.y, 0, 'f', 3) - .arg(dstPoint.z, 0, 'f', 3)); - - updateStatusBar("坐标变换完成"); -} - -void CalibViewMainWindow::onEulerTest() -{ - if (!m_calib) { - QMessageBox::critical(this, "错误", "标定实例未初始化"); - return; - } - - HECEulerAngles inputAngles = HECEulerAngles::fromDegrees( - m_sbRoll->value(), - m_sbPitch->value(), - m_sbYaw->value() - ); - HECEulerOrder order = static_cast(m_cbEulerOrder->currentData().toInt()); - - // 欧拉角 -> 旋转矩阵 - HECRotationMatrix R; - m_calib->EulerToRotationMatrix(inputAngles, order, R); - - // 旋转矩阵 -> 欧拉角 - HECEulerAngles outputAngles; - m_calib->RotationMatrixToEuler(R, order, outputAngles); - - // 更新结果显示 - m_resultWidget->updateRotationDisplay(R); - - // 获取顺序名称 - QString orderName; - switch (order) { - case HECEulerOrder::XYZ: orderName = "XYZ"; break; - case HECEulerOrder::XZY: orderName = "XZY"; break; - case HECEulerOrder::YXZ: orderName = "YXZ"; break; - case HECEulerOrder::YZX: orderName = "YZX"; break; - case HECEulerOrder::ZXY: orderName = "ZXY"; break; - case HECEulerOrder::ZYX: orderName = "ZYX"; break; - } - - double inRoll, inPitch, inYaw; - inputAngles.toDegrees(inRoll, inPitch, inYaw); - double outRoll, outPitch, outYaw; - outputAngles.toDegrees(outRoll, outPitch, outYaw); - - appendLog(QString("欧拉角转换 (外旋顺序: %1):").arg(orderName)); - appendLog(QString(" 输入: Roll=%1\302\260, Pitch=%2\302\260, Yaw=%3\302\260") - .arg(inRoll, 0, 'f', 2).arg(inPitch, 0, 'f', 2).arg(inYaw, 0, 'f', 2)); - appendLog(QString(" 输出: Roll=%1\302\260, Pitch=%2\302\260, Yaw=%3\302\260") - .arg(outRoll, 0, 'f', 2).arg(outPitch, 0, 'f', 2).arg(outYaw, 0, 'f', 2)); - - updateStatusBar("欧拉角转换完成"); -} - -void CalibViewMainWindow::onClearAll() -{ - m_dataWidget->clearAll(); - m_resultWidget->clearAll(); - m_logEdit->clear(); - m_sbTransformX->setValue(0); - m_sbTransformY->setValue(0); - m_sbTransformZ->setValue(0); - m_sbRoll->setValue(0); - m_sbPitch->setValue(0); - m_sbYaw->setValue(0); - m_hasResult = false; - updateStatusBar("已清除所有数据"); -} - -void CalibViewMainWindow::onSaveResult() -{ - if (!m_hasResult) { - QMessageBox::warning(this, "警告", "没有可保存的标定结果"); - return; - } - - QString fileName = QFileDialog::getSaveFileName(this, - "保存标定结果", "", "JSON文件 (*.json)"); - - if (fileName.isEmpty()) { - return; - } - - QJsonObject root; - - // 保存旋转矩阵 - QJsonArray rotArray; - for (int i = 0; i < 9; ++i) { - rotArray.append(m_currentResult.R.data[i]); - } - root["rotation"] = rotArray; - - // 保存平移向量 - QJsonArray transArray; - for (int i = 0; i < 3; ++i) { - transArray.append(m_currentResult.T.data[i]); - } - root["translation"] = transArray; - - // 保存质心 - QJsonObject centerEye; - centerEye["x"] = m_currentResult.centerEye.x; - centerEye["y"] = m_currentResult.centerEye.y; - centerEye["z"] = m_currentResult.centerEye.z; - root["centerEye"] = centerEye; - - QJsonObject centerRobot; - centerRobot["x"] = m_currentResult.centerRobot.x; - centerRobot["y"] = m_currentResult.centerRobot.y; - centerRobot["z"] = m_currentResult.centerRobot.z; - root["centerRobot"] = centerRobot; - - // 保存误差 - root["error"] = m_currentResult.error; - - QFile file(fileName); - if (file.open(QIODevice::WriteOnly)) { - QJsonDocument doc(root); - file.write(doc.toJson(QJsonDocument::Indented)); - file.close(); - appendLog(QString("结果已保存到: %1").arg(fileName)); - updateStatusBar("结果已保存"); - } else { - QMessageBox::critical(this, "错误", "无法保存文件"); - } -} - -void CalibViewMainWindow::onLoadResult() -{ - QString fileName = QFileDialog::getOpenFileName(this, - "加载标定结果", "", "JSON文件 (*.json)"); - - if (fileName.isEmpty()) { - return; - } - - QFile file(fileName); - if (!file.open(QIODevice::ReadOnly)) { - QMessageBox::critical(this, "错误", "无法打开文件"); - return; - } - - QByteArray data = file.readAll(); - file.close(); - - QJsonDocument doc = QJsonDocument::fromJson(data); - if (doc.isNull()) { - QMessageBox::critical(this, "错误", "无效的JSON文件"); - return; - } - - QJsonObject root = doc.object(); - - // 加载旋转矩阵 - QJsonArray rotArray = root["rotation"].toArray(); - if (rotArray.size() == 9) { - for (int i = 0; i < 9; ++i) { - m_currentResult.R.data[i] = rotArray[i].toDouble(); - } - } - - // 加载平移向量 - QJsonArray transArray = root["translation"].toArray(); - if (transArray.size() == 3) { - for (int i = 0; i < 3; ++i) { - m_currentResult.T.data[i] = transArray[i].toDouble(); - } - } - - // 加载质心 - QJsonObject centerEye = root["centerEye"].toObject(); - m_currentResult.centerEye.x = centerEye["x"].toDouble(); - m_currentResult.centerEye.y = centerEye["y"].toDouble(); - m_currentResult.centerEye.z = centerEye["z"].toDouble(); - - QJsonObject centerRobot = root["centerRobot"].toObject(); - m_currentResult.centerRobot.x = centerRobot["x"].toDouble(); - m_currentResult.centerRobot.y = centerRobot["y"].toDouble(); - m_currentResult.centerRobot.z = centerRobot["z"].toDouble(); - - // 加载误差 - m_currentResult.error = root["error"].toDouble(); - - m_hasResult = true; - m_resultWidget->showCalibResult(m_currentResult); - appendLog(QString("已加载标定结果: %1").arg(fileName)); - updateStatusBar("标定结果已加载"); -} - -void CalibViewMainWindow::onOpenRobotView() -{ - if (!m_robotView) { - m_robotView = new MainWindow(this); - connect(m_robotView, &MainWindow::tcpPoseUpdated, - this, &CalibViewMainWindow::onRobotTcpPoseReceived); - } - m_robotView->show(); - m_robotView->raise(); - m_robotView->activateWindow(); -} - -void CalibViewMainWindow::onRobotTcpPoseReceived( - double x, double y, double z, double rx, double ry, double rz) -{ - m_dataWidget->setRobotInput(x, y, z, rx, ry, rz); - appendLog(QString("收到机器人数据: (%1, %2, %3, %4, %5, %6)") - .arg(x, 0, 'f', 2).arg(y, 0, 'f', 2).arg(z, 0, 'f', 2) - .arg(rx, 0, 'f', 2).arg(ry, 0, 'f', 2).arg(rz, 0, 'f', 2)); -} - -void CalibViewMainWindow::onOpenVrEyeView() -{ - if (!m_vrEyeView) { - m_vrEyeView = new VrEyeViewWidget(); // 不设置父窗口,独立窗口 - - // 连接信号槽 - connect(m_vrEyeView, &VrEyeViewWidget::chessboardDetected, - this, [this](const ChessboardDetectionData& data) { - if (data.detected) { - onChessboardDetected(data.x, data.y, data.z, data.rx, data.ry, data.rz); - } - }); - - // 设置为独立窗口 - m_vrEyeView->setWindowFlags(Qt::Window); - m_vrEyeView->resize(900, 700); - m_vrEyeView->setWindowTitle("相机标定板检测 - VrEyeView"); - } - - m_vrEyeView->show(); - m_vrEyeView->raise(); - m_vrEyeView->activateWindow(); -} - -void CalibViewMainWindow::onChessboardDetected( - double x, double y, double z, double rx, double ry, double rz) -{ - // 将标定板检测结果作为相机坐标输入到数据控件 - m_dataWidget->setCameraInput(x, y, z, rx, ry, rz); - appendLog(QString("收到标定板检测数据: 位置(%1, %2, %3) 姿态(%4°, %5°, %6°)") - .arg(x, 0, 'f', 2).arg(y, 0, 'f', 2).arg(z, 0, 'f', 2) - .arg(rx, 0, 'f', 2).arg(ry, 0, 'f', 2).arg(rz, 0, 'f', 2)); -} +#include "CalibViewMainWindow.h" +#include "CalibDataWidget.h" +#include "CalibResultWidget.h" +#include "MainWindow.h" +#include "VrEyeViewWidget.h" +#include "../../SpinBoxPasteHelper.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +CalibViewMainWindow::CalibViewMainWindow(QWidget* parent) + : QMainWindow(parent) + , m_calib(nullptr) + , m_dataWidget(nullptr) + , m_resultWidget(nullptr) + , m_sbTransformX(nullptr) + , m_sbTransformY(nullptr) + , m_sbTransformZ(nullptr) + , m_btnTransform(nullptr) + , m_sbRoll(nullptr) + , m_sbPitch(nullptr) + , m_sbYaw(nullptr) + , m_cbEulerOrder(nullptr) + , m_btnEulerConvert(nullptr) + , m_logEdit(nullptr) + , m_hasResult(false) + , m_robotView(nullptr) + , m_vrEyeView(nullptr) +{ + // 创建标定实例 + m_calib = CreateHandEyeCalibInstance(); + + setupUI(); + createMenuBar(); + SpinBoxPasteHelper::install(this); + + setWindowTitle("CalibView - 手眼标定测试工具"); + resize(1200, 650); + + updateStatusBar("就绪"); +} + +CalibViewMainWindow::~CalibViewMainWindow() +{ + if (m_calib) { + DestroyHandEyeCalibInstance(m_calib); + m_calib = nullptr; + } +} + +QWidget* CalibViewMainWindow::createRightPanel() +{ + QWidget* rightPanel = new QWidget(this); + QVBoxLayout* rightLayout = new QVBoxLayout(rightPanel); + + // 坐标变换测试组 + QGroupBox* transformGroup = new QGroupBox("坐标变换测试", this); + QGridLayout* transformLayout = new QGridLayout(transformGroup); + + transformLayout->addWidget(new QLabel("X:", this), 0, 0); + m_sbTransformX = new QDoubleSpinBox(this); + m_sbTransformX->setRange(-10000, 10000); + m_sbTransformX->setDecimals(3); + transformLayout->addWidget(m_sbTransformX, 0, 1); + + transformLayout->addWidget(new QLabel("Y:", this), 0, 2); + m_sbTransformY = new QDoubleSpinBox(this); + m_sbTransformY->setRange(-10000, 10000); + m_sbTransformY->setDecimals(3); + transformLayout->addWidget(m_sbTransformY, 0, 3); + + transformLayout->addWidget(new QLabel("Z:", this), 0, 4); + m_sbTransformZ = new QDoubleSpinBox(this); + m_sbTransformZ->setRange(-10000, 10000); + m_sbTransformZ->setDecimals(3); + transformLayout->addWidget(m_sbTransformZ, 0, 5); + + m_btnTransform = new QPushButton("变换", this); + connect(m_btnTransform, &QPushButton::clicked, this, &CalibViewMainWindow::onTransformTest); + transformLayout->addWidget(m_btnTransform, 1, 0, 1, 6); + + rightLayout->addWidget(transformGroup); + + // 欧拉角转换测试组 + QGroupBox* eulerGroup = new QGroupBox("欧拉角转换测试", this); + QGridLayout* eulerLayout = new QGridLayout(eulerGroup); + + eulerLayout->addWidget(new QLabel("Roll (\302\260):", this), 0, 0); + m_sbRoll = new QDoubleSpinBox(this); + m_sbRoll->setRange(-180, 180); + m_sbRoll->setDecimals(2); + eulerLayout->addWidget(m_sbRoll, 0, 1); + + eulerLayout->addWidget(new QLabel("Pitch (\302\260):", this), 0, 2); + m_sbPitch = new QDoubleSpinBox(this); + m_sbPitch->setRange(-180, 180); + m_sbPitch->setDecimals(2); + eulerLayout->addWidget(m_sbPitch, 0, 3); + + eulerLayout->addWidget(new QLabel("Yaw (\302\260):", this), 0, 4); + m_sbYaw = new QDoubleSpinBox(this); + m_sbYaw->setRange(-180, 180); + m_sbYaw->setDecimals(2); + eulerLayout->addWidget(m_sbYaw, 0, 5); + + eulerLayout->addWidget(new QLabel("旋转顺序:", this), 1, 0); + m_cbEulerOrder = new QComboBox(this); + m_cbEulerOrder->addItem("XYZ", static_cast(HECEulerOrder::XYZ)); + m_cbEulerOrder->addItem("XZY", static_cast(HECEulerOrder::XZY)); + m_cbEulerOrder->addItem("YXZ", static_cast(HECEulerOrder::YXZ)); + m_cbEulerOrder->addItem("YZX", static_cast(HECEulerOrder::YZX)); + m_cbEulerOrder->addItem("ZXY", static_cast(HECEulerOrder::ZXY)); + m_cbEulerOrder->addItem("ZYX (常用)", static_cast(HECEulerOrder::ZYX)); + m_cbEulerOrder->setCurrentIndex(5); // 默认 ZYX + eulerLayout->addWidget(m_cbEulerOrder, 1, 1, 1, 2); + + m_btnEulerConvert = new QPushButton("转换", this); + connect(m_btnEulerConvert, &QPushButton::clicked, this, &CalibViewMainWindow::onEulerTest); + eulerLayout->addWidget(m_btnEulerConvert, 1, 3, 1, 3); + + rightLayout->addWidget(eulerGroup); + + // 日志组 + QGroupBox* logGroup = new QGroupBox("日志", this); + QVBoxLayout* logLayout = new QVBoxLayout(logGroup); + + m_logEdit = new QTextEdit(this); + m_logEdit->setReadOnly(true); + m_logEdit->setFont(QFont("Consolas", 9)); + + logLayout->addWidget(m_logEdit); + + rightLayout->addWidget(logGroup, 1); // stretch=1 让日志区占据剩余空间 + + return rightPanel; +} + +void CalibViewMainWindow::setupUI() +{ + // 创建中央控件 + QWidget* centralWidget = new QWidget(this); + QHBoxLayout* mainLayout = new QHBoxLayout(centralWidget); + + // 创建分割器 + QSplitter* splitter = new QSplitter(Qt::Horizontal, this); + + // 左侧面板:数据输入 + 标定结果 + QWidget* leftPanel = new QWidget(this); + QVBoxLayout* leftLayout = new QVBoxLayout(leftPanel); + + // 数据输入(可滚动) + QScrollArea* dataScroll = new QScrollArea(this); + m_dataWidget = new CalibDataWidget(this); + dataScroll->setWidget(m_dataWidget); + dataScroll->setWidgetResizable(true); + leftLayout->addWidget(dataScroll, 1); + + // 标定结果 + m_resultWidget = new CalibResultWidget(this); + leftLayout->addWidget(m_resultWidget); + + splitter->addWidget(leftPanel); + + // 右侧面板:测试工具 + 日志 + QWidget* rightPanel = createRightPanel(); + splitter->addWidget(rightPanel); + + // 设置分割比例 + splitter->setStretchFactor(0, 2); + splitter->setStretchFactor(1, 1); + + mainLayout->addWidget(splitter); + setCentralWidget(centralWidget); + + // 创建状态栏 + statusBar()->showMessage("就绪"); + + // 连接 CalibDataWidget 的信号 + connect(m_dataWidget, &CalibDataWidget::requestEyeToHandCalib, + this, &CalibViewMainWindow::onEyeToHandCalib); + connect(m_dataWidget, &CalibDataWidget::requestEyeInHandCalib, + this, &CalibViewMainWindow::onEyeInHandCalib); + connect(m_dataWidget, &CalibDataWidget::requestTCPCalib, + this, &CalibViewMainWindow::onTCPCalib); +} + +void CalibViewMainWindow::createMenuBar() +{ + // 文件菜单 + QMenu* fileMenu = menuBar()->addMenu("文件(&F)"); + + QAction* actSave = fileMenu->addAction("保存结果(&S)"); + actSave->setShortcut(QKeySequence::Save); + connect(actSave, &QAction::triggered, this, &CalibViewMainWindow::onSaveResult); + + QAction* actLoad = fileMenu->addAction("加载结果(&L)"); + actLoad->setShortcut(QKeySequence::Open); + connect(actLoad, &QAction::triggered, this, &CalibViewMainWindow::onLoadResult); + + fileMenu->addSeparator(); + + QAction* actSaveData = fileMenu->addAction("保存标定数据(&D)"); + actSaveData->setShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_S)); + connect(actSaveData, &QAction::triggered, this, &CalibViewMainWindow::onSaveCalibData); + + QAction* actLoadData = fileMenu->addAction("加载标定数据(&A)"); + actLoadData->setShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_O)); + connect(actLoadData, &QAction::triggered, this, &CalibViewMainWindow::onLoadCalibData); + + fileMenu->addSeparator(); + + QAction* actExit = fileMenu->addAction("退出(&X)"); + actExit->setShortcut(QKeySequence::Quit); + connect(actExit, &QAction::triggered, this, &QMainWindow::close); + + // 标定菜单 + QMenu* calibMenu = menuBar()->addMenu("标定(&C)"); + + QAction* actEyeToHand = calibMenu->addAction("Eye-To-Hand 标定(&E)"); + connect(actEyeToHand, &QAction::triggered, this, &CalibViewMainWindow::onEyeToHandCalib); + + QAction* actEyeInHand = calibMenu->addAction("Eye-In-Hand 标定(&I)"); + connect(actEyeInHand, &QAction::triggered, this, &CalibViewMainWindow::onEyeInHandCalib); + + QAction* actTCPCalib = calibMenu->addAction("TCP 标定(&P)"); + connect(actTCPCalib, &QAction::triggered, this, &CalibViewMainWindow::onTCPCalib); + + calibMenu->addSeparator(); + + QAction* actTransform = calibMenu->addAction("坐标变换测试(&T)"); + connect(actTransform, &QAction::triggered, this, &CalibViewMainWindow::onTransformTest); + + QAction* actEuler = calibMenu->addAction("欧拉角转换测试(&U)"); + connect(actEuler, &QAction::triggered, this, &CalibViewMainWindow::onEulerTest); + + calibMenu->addSeparator(); + + QAction* actClear = calibMenu->addAction("清除所有(&C)"); + connect(actClear, &QAction::triggered, this, &CalibViewMainWindow::onClearAll); + + // 工具菜单 + QMenu* toolMenu = menuBar()->addMenu("工具(&T)"); + QAction* actRobotView = toolMenu->addAction("机器人控制(&R)"); + connect(actRobotView, &QAction::triggered, this, &CalibViewMainWindow::onOpenRobotView); + + QAction* actVrEyeView = toolMenu->addAction("相机标定板检测(&V)"); + connect(actVrEyeView, &QAction::triggered, this, &CalibViewMainWindow::onOpenVrEyeView); + + // 帮助菜单 + QMenu* helpMenu = menuBar()->addMenu("帮助(&H)"); + + QAction* actAbout = helpMenu->addAction("关于(&A)"); + connect(actAbout, &QAction::triggered, this, [this]() { + QMessageBox::about(this, "关于 CalibView", + "CalibView - 手眼标定测试工具\n\n" + "用于测试 HandEyeCalib 模块的各项功能:\n" + "- Eye-To-Hand 标定\n" + "- Eye-In-Hand 标定\n" + "- TCP 标定\n" + "- 坐标变换\n" + "- 欧拉角转换\n\n" + "基于 Eigen 库实现的 SVD 分解算法"); + }); +} + +void CalibViewMainWindow::updateStatusBar(const QString& message) +{ + statusBar()->showMessage(message); +} + +void CalibViewMainWindow::appendLog(const QString& message) +{ + m_logEdit->append(message); +} + +void CalibViewMainWindow::onEyeToHandCalib() +{ + if (!m_calib) { + QMessageBox::critical(this, "错误", "标定实例未初始化"); + return; + } + + std::vector eyePoints; + std::vector robotPoints; + m_dataWidget->getEyeToHandData(eyePoints, robotPoints); + + if (eyePoints.size() < 3) { + QMessageBox::warning(this, "警告", "至少需要3组对应点进行标定"); + return; + } + + appendLog("开始 Eye-To-Hand 标定..."); + appendLog(QString("输入点数: %1").arg(eyePoints.size())); + + int ret = m_calib->CalculateRT(eyePoints, robotPoints, m_currentResult); + + if (ret == 0) { + m_hasResult = true; + m_resultWidget->showCalibResult(m_currentResult); + appendLog(QString("标定成功,误差: %1 mm") + .arg(m_currentResult.error, 0, 'f', 4)); + updateStatusBar("Eye-To-Hand 标定完成"); + emit calibrationCompleted(m_currentResult); + } else { + appendLog(QString("标定失败,错误码: %1").arg(ret)); + QMessageBox::critical(this, "错误", QString("标定失败,错误码: %1").arg(ret)); + } +} + +void CalibViewMainWindow::onEyeInHandCalib() +{ + if (!m_calib) { + QMessageBox::critical(this, "错误", "标定实例未初始化"); + return; + } + + std::vector calibData; + m_dataWidget->getEyeInHandData(calibData); + + if (calibData.size() < 3) { + QMessageBox::warning(this, "警告", "至少需要3组数据进行标定"); + return; + } + + appendLog("开始 Eye-In-Hand 标定..."); + appendLog(QString("输入数据组数: %1").arg(calibData.size())); + + int ret = m_calib->CalculateEyeInHand(calibData, m_currentResult); + + if (ret == 0) { + m_hasResult = true; + m_resultWidget->showCalibResult(m_currentResult); + appendLog(QString("标定成功,误差: %1 mm") + .arg(m_currentResult.error, 0, 'f', 4)); + updateStatusBar("Eye-In-Hand 标定完成"); + emit calibrationCompleted(m_currentResult); + } else { + appendLog(QString("标定失败,错误码: %1").arg(ret)); + QMessageBox::critical(this, "错误", QString("标定失败,错误码: %1").arg(ret)); + } +} + +void CalibViewMainWindow::onTCPCalib() +{ + if (!m_calib) { + QMessageBox::critical(this, "错误", "标定实例未初始化"); + return; + } + + HECTCPCalibData tcpData = m_dataWidget->getTCPCalibData(); + + if (tcpData.poses.size() < 3) { + QMessageBox::warning(this, "警告", "至少需要3组法兰位姿进行TCP标定"); + return; + } + + if (tcpData.mode == HECTCPCalibMode::Full6DOF) { + if (tcpData.referencePoseIndex < 0 || + tcpData.referencePoseIndex >= static_cast(tcpData.poses.size())) { + QMessageBox::warning(this, "警告", + QString("参考位姿索引 %1 越界,有效范围: 0-%2") + .arg(tcpData.referencePoseIndex) + .arg(tcpData.poses.size() - 1)); + return; + } + } + + QString modeName = (tcpData.mode == HECTCPCalibMode::PositionOnly) ? + "3-DOF 位置标定" : "6-DOF 完整标定"; + appendLog(QString("开始 TCP 标定 (%1)...").arg(modeName)); + appendLog(QString("输入位姿数: %1").arg(tcpData.poses.size())); + + HECTCPCalibResult tcpResult = m_calib->CalculateTCP(tcpData); + + if (tcpResult.success) { + m_resultWidget->showTCPCalibResult(tcpResult); + + appendLog("=== TCP 标定结果 ==="); + appendLog(QString("TCP 位置偏移: tx=%1, ty=%2, tz=%3") + .arg(tcpResult.tx, 0, 'f', 3) + .arg(tcpResult.ty, 0, 'f', 3) + .arg(tcpResult.tz, 0, 'f', 3)); + if (tcpResult.rx != 0 || tcpResult.ry != 0 || tcpResult.rz != 0) { + appendLog(QString("TCP 姿态偏移: rx=%1\302\260, ry=%2\302\260, rz=%3\302\260") + .arg(tcpResult.rx, 0, 'f', 2) + .arg(tcpResult.ry, 0, 'f', 2) + .arg(tcpResult.rz, 0, 'f', 2)); + } + appendLog(QString("残差误差: %1 mm").arg(tcpResult.residualError, 0, 'f', 4)); + appendLog("TCP 标定成功"); + updateStatusBar("TCP 标定完成"); + } else { + appendLog(QString("TCP 标定失败: %1") + .arg(QString::fromStdString(tcpResult.errorMessage))); + QMessageBox::critical(this, "错误", + QString("TCP 标定失败: %1").arg(QString::fromStdString(tcpResult.errorMessage))); + } +} + +void CalibViewMainWindow::onTransformTest() +{ + if (!m_calib) { + QMessageBox::critical(this, "错误", "标定实例未初始化"); + return; + } + + if (!m_hasResult) { + QMessageBox::warning(this, "警告", "请先执行标定或加载标定结果"); + return; + } + + HECPoint3D srcPoint( + m_sbTransformX->value(), + m_sbTransformY->value(), + m_sbTransformZ->value() + ); + HECPoint3D dstPoint; + + m_calib->TransformPoint(m_currentResult.R, m_currentResult.T, srcPoint, dstPoint); + + appendLog(QString("坐标变换结果:")); + appendLog(QString(" 源点: (%1, %2, %3)") + .arg(srcPoint.x, 0, 'f', 3) + .arg(srcPoint.y, 0, 'f', 3) + .arg(srcPoint.z, 0, 'f', 3)); + appendLog(QString(" 目标点: (%1, %2, %3)") + .arg(dstPoint.x, 0, 'f', 3) + .arg(dstPoint.y, 0, 'f', 3) + .arg(dstPoint.z, 0, 'f', 3)); + + updateStatusBar("坐标变换完成"); +} + +void CalibViewMainWindow::onEulerTest() +{ + if (!m_calib) { + QMessageBox::critical(this, "错误", "标定实例未初始化"); + return; + } + + HECEulerAngles inputAngles = HECEulerAngles::fromDegrees( + m_sbRoll->value(), + m_sbPitch->value(), + m_sbYaw->value() + ); + HECEulerOrder order = static_cast(m_cbEulerOrder->currentData().toInt()); + + // 欧拉角 -> 旋转矩阵 + HECRotationMatrix R; + m_calib->EulerToRotationMatrix(inputAngles, order, R); + + // 旋转矩阵 -> 欧拉角 + HECEulerAngles outputAngles; + m_calib->RotationMatrixToEuler(R, order, outputAngles); + + // 更新结果显示 + m_resultWidget->updateRotationDisplay(R); + + // 获取顺序名称 + QString orderName; + switch (order) { + case HECEulerOrder::XYZ: orderName = "XYZ"; break; + case HECEulerOrder::XZY: orderName = "XZY"; break; + case HECEulerOrder::YXZ: orderName = "YXZ"; break; + case HECEulerOrder::YZX: orderName = "YZX"; break; + case HECEulerOrder::ZXY: orderName = "ZXY"; break; + case HECEulerOrder::ZYX: orderName = "ZYX"; break; + } + + double inRoll, inPitch, inYaw; + inputAngles.toDegrees(inRoll, inPitch, inYaw); + double outRoll, outPitch, outYaw; + outputAngles.toDegrees(outRoll, outPitch, outYaw); + + appendLog(QString("欧拉角转换 (外旋顺序: %1):").arg(orderName)); + appendLog(QString(" 输入: Roll=%1\302\260, Pitch=%2\302\260, Yaw=%3\302\260") + .arg(inRoll, 0, 'f', 2).arg(inPitch, 0, 'f', 2).arg(inYaw, 0, 'f', 2)); + appendLog(QString(" 输出: Roll=%1\302\260, Pitch=%2\302\260, Yaw=%3\302\260") + .arg(outRoll, 0, 'f', 2).arg(outPitch, 0, 'f', 2).arg(outYaw, 0, 'f', 2)); + + updateStatusBar("欧拉角转换完成"); +} + +void CalibViewMainWindow::onClearAll() +{ + m_dataWidget->clearAll(); + m_resultWidget->clearAll(); + m_logEdit->clear(); + m_sbTransformX->setValue(0); + m_sbTransformY->setValue(0); + m_sbTransformZ->setValue(0); + m_sbRoll->setValue(0); + m_sbPitch->setValue(0); + m_sbYaw->setValue(0); + m_hasResult = false; + updateStatusBar("已清除所有数据"); +} + +void CalibViewMainWindow::onSaveResult() +{ + if (!m_hasResult) { + QMessageBox::warning(this, "警告", "没有可保存的标定结果"); + return; + } + + QString defaultName = QString("CalibResult_%1.ini") + .arg(QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss")); + + QString fileName = QFileDialog::getSaveFileName(this, + "保存标定结果", defaultName, "INI文件 (*.ini)"); + + if (fileName.isEmpty()) { + return; + } + + QSettings ini(fileName, QSettings::IniFormat); + ini.setIniCodec("UTF-8"); + + // [CommInfo] + ini.beginGroup("CommInfo"); + ini.setValue("sCalibTime", QDateTime::currentDateTime().toString("yyyy-MM-dd-HH-mm-ss")); + ini.setValue("dError", m_currentResult.error); + ini.endGroup(); + + // [CalibMatrixInfo_0] - 4x4 齐次矩阵 + ini.beginGroup("CalibMatrixInfo_0"); + // 第0行: R[0,0] R[0,1] R[0,2] Tx + ini.setValue("dCalibMatrix_0", m_currentResult.R.data[0]); + ini.setValue("dCalibMatrix_1", m_currentResult.R.data[1]); + ini.setValue("dCalibMatrix_2", m_currentResult.R.data[2]); + ini.setValue("dCalibMatrix_3", m_currentResult.T.data[0]); + // 第1行: R[1,0] R[1,1] R[1,2] Ty + ini.setValue("dCalibMatrix_4", m_currentResult.R.data[3]); + ini.setValue("dCalibMatrix_5", m_currentResult.R.data[4]); + ini.setValue("dCalibMatrix_6", m_currentResult.R.data[5]); + ini.setValue("dCalibMatrix_7", m_currentResult.T.data[1]); + // 第2行: R[2,0] R[2,1] R[2,2] Tz + ini.setValue("dCalibMatrix_8", m_currentResult.R.data[6]); + ini.setValue("dCalibMatrix_9", m_currentResult.R.data[7]); + ini.setValue("dCalibMatrix_10", m_currentResult.R.data[8]); + ini.setValue("dCalibMatrix_11", m_currentResult.T.data[2]); + // 第3行: 0 0 0 1 + ini.setValue("dCalibMatrix_12", 0.0); + ini.setValue("dCalibMatrix_13", 0.0); + ini.setValue("dCalibMatrix_14", 0.0); + ini.setValue("dCalibMatrix_15", 1.0); + ini.endGroup(); + + // [CenterInfo] - 质心 + ini.beginGroup("CenterInfo"); + ini.setValue("dCenterEyeX", m_currentResult.centerEye.x); + ini.setValue("dCenterEyeY", m_currentResult.centerEye.y); + ini.setValue("dCenterEyeZ", m_currentResult.centerEye.z); + ini.setValue("dCenterRobotX", m_currentResult.centerRobot.x); + ini.setValue("dCenterRobotY", m_currentResult.centerRobot.y); + ini.setValue("dCenterRobotZ", m_currentResult.centerRobot.z); + ini.endGroup(); + + appendLog(QString("结果已保存到: %1").arg(fileName)); + updateStatusBar("结果已保存"); +} + +void CalibViewMainWindow::onLoadResult() +{ + QString fileName = QFileDialog::getOpenFileName(this, + "加载标定结果", "", "INI文件 (*.ini)"); + + if (fileName.isEmpty()) { + return; + } + + QSettings ini(fileName, QSettings::IniFormat); + ini.setIniCodec("UTF-8"); + + // 校验是否包含标定矩阵 + ini.beginGroup("CalibMatrixInfo_0"); + bool hasMatrix = ini.contains("dCalibMatrix_0"); + if (!hasMatrix) { + ini.endGroup(); + QMessageBox::warning(this, "警告", "该文件不包含有效的标定结果"); + return; + } + + // 加载 4x4 齐次矩阵 → 拆分为 R[3x3] + T[3x1] + // 第0行 + m_currentResult.R.data[0] = ini.value("dCalibMatrix_0", 0).toDouble(); + m_currentResult.R.data[1] = ini.value("dCalibMatrix_1", 0).toDouble(); + m_currentResult.R.data[2] = ini.value("dCalibMatrix_2", 0).toDouble(); + m_currentResult.T.data[0] = ini.value("dCalibMatrix_3", 0).toDouble(); + // 第1行 + m_currentResult.R.data[3] = ini.value("dCalibMatrix_4", 0).toDouble(); + m_currentResult.R.data[4] = ini.value("dCalibMatrix_5", 0).toDouble(); + m_currentResult.R.data[5] = ini.value("dCalibMatrix_6", 0).toDouble(); + m_currentResult.T.data[1] = ini.value("dCalibMatrix_7", 0).toDouble(); + // 第2行 + m_currentResult.R.data[6] = ini.value("dCalibMatrix_8", 0).toDouble(); + m_currentResult.R.data[7] = ini.value("dCalibMatrix_9", 0).toDouble(); + m_currentResult.R.data[8] = ini.value("dCalibMatrix_10", 0).toDouble(); + m_currentResult.T.data[2] = ini.value("dCalibMatrix_11", 0).toDouble(); + ini.endGroup(); + + // 加载误差 + ini.beginGroup("CommInfo"); + m_currentResult.error = ini.value("dError", 0).toDouble(); + ini.endGroup(); + + // 加载质心 + ini.beginGroup("CenterInfo"); + m_currentResult.centerEye.x = ini.value("dCenterEyeX", 0).toDouble(); + m_currentResult.centerEye.y = ini.value("dCenterEyeY", 0).toDouble(); + m_currentResult.centerEye.z = ini.value("dCenterEyeZ", 0).toDouble(); + m_currentResult.centerRobot.x = ini.value("dCenterRobotX", 0).toDouble(); + m_currentResult.centerRobot.y = ini.value("dCenterRobotY", 0).toDouble(); + m_currentResult.centerRobot.z = ini.value("dCenterRobotZ", 0).toDouble(); + ini.endGroup(); + + m_hasResult = true; + m_resultWidget->showCalibResult(m_currentResult); + appendLog(QString("已加载标定结果: %1").arg(fileName)); + updateStatusBar("标定结果已加载"); +} + +void CalibViewMainWindow::onOpenRobotView() +{ + if (!m_robotView) { + m_robotView = new MainWindow(this); + connect(m_robotView, &MainWindow::tcpPoseUpdated, + this, &CalibViewMainWindow::onRobotTcpPoseReceived); + } + m_robotView->show(); + m_robotView->raise(); + m_robotView->activateWindow(); +} + +void CalibViewMainWindow::onRobotTcpPoseReceived( + double x, double y, double z, double rx, double ry, double rz) +{ + m_dataWidget->setRobotInput(x, y, z, rx, ry, rz); + appendLog(QString("收到机器人数据: (%1, %2, %3, %4, %5, %6)") + .arg(x, 0, 'f', 2).arg(y, 0, 'f', 2).arg(z, 0, 'f', 2) + .arg(rx, 0, 'f', 2).arg(ry, 0, 'f', 2).arg(rz, 0, 'f', 2)); +} + +void CalibViewMainWindow::onOpenVrEyeView() +{ + if (!m_vrEyeView) { + m_vrEyeView = new VrEyeViewWidget(); // 不设置父窗口,独立窗口 + + // 连接信号槽 + connect(m_vrEyeView, &VrEyeViewWidget::chessboardDetected, + this, [this](const ChessboardDetectionData& data) { + if (data.detected) { + onChessboardDetected(data.x, data.y, data.z, data.rx, data.ry, data.rz); + } + }); + + // 设置为独立窗口 + m_vrEyeView->setWindowFlags(Qt::Window); + m_vrEyeView->resize(900, 700); + m_vrEyeView->setWindowTitle("相机标定板检测 - VrEyeView"); + } + + m_vrEyeView->show(); + m_vrEyeView->raise(); + m_vrEyeView->activateWindow(); +} + +void CalibViewMainWindow::onChessboardDetected( + double x, double y, double z, double rx, double ry, double rz) +{ + // 将标定板检测结果作为相机坐标输入到数据控件 + m_dataWidget->setCameraInput(x, y, z, rx, ry, rz); + appendLog(QString("收到标定板检测数据: 位置(%1, %2, %3) 姿态(%4°, %5°, %6°)") + .arg(x, 0, 'f', 2).arg(y, 0, 'f', 2).arg(z, 0, 'f', 2) + .arg(rx, 0, 'f', 2).arg(ry, 0, 'f', 2).arg(rz, 0, 'f', 2)); +} + +void CalibViewMainWindow::onSaveCalibData() +{ + // 根据当前模式生成默认文件名 + static const char* typeNames[] = { "EyeToHand", "EyeInHand", "TCP" }; + int typeIndex = m_dataWidget->getCalibTypeIndex(); + QString typeName = typeNames[typeIndex]; + + QString defaultName = QString("CalibData_%1_%2.ini") + .arg(typeName) + .arg(QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss")); + + QString fileName = QFileDialog::getSaveFileName(this, + "保存标定数据", defaultName, "INI文件 (*.ini)"); + + if (fileName.isEmpty()) { + return; + } + + m_dataWidget->saveCalibData(fileName); + appendLog(QString("标定数据已保存到: %1").arg(fileName)); + updateStatusBar("标定数据已保存"); +} + +void CalibViewMainWindow::onLoadCalibData() +{ + QString fileName = QFileDialog::getOpenFileName(this, + "加载标定数据", "", "INI文件 (*.ini)"); + + if (fileName.isEmpty()) { + return; + } + + // 校验文件是否包含标定数据 + QSettings check(fileName, QSettings::IniFormat); + check.beginGroup("CommInfo"); + int calibType = check.value("eCalibType", -1).toInt(); + QString saveTime = check.value("sCalibTime").toString(); + check.endGroup(); + + if (calibType < 0 || calibType > 2) { + QMessageBox::warning(this, "警告", "该文件不包含有效的标定数据"); + return; + } + + m_dataWidget->loadCalibData(fileName); + + static const char* typeNames[] = { "EyeToHand", "EyeInHand", "TCP" }; + appendLog(QString("已加载标定数据: %1 (保存时间: %2, 模式: %3)") + .arg(fileName).arg(saveTime).arg(typeNames[calibType])); + updateStatusBar("标定数据已加载"); +} diff --git a/Utils b/Utils index 7071da6..8c8dd5a 160000 --- a/Utils +++ b/Utils @@ -1 +1 @@ -Subproject commit 7071da62403ea703981ffe77fff948266b66fdf3 +Subproject commit 8c8dd5a5a26a316dbcd523eddfb8bd56727c62d2