#include "CalibViewMainWindow.h" #include "CalibDataWidget.h" #include "CalibResultWidget.h" #include "MainWindow.h" #include "VrEyeViewWidget.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(); 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)); }