675 lines
23 KiB
C++
675 lines
23 KiB
C++
#include "CalibViewMainWindow.h"
|
|
#include "CalibDataWidget.h"
|
|
#include "CalibResultWidget.h"
|
|
#include "MainWindow.h"
|
|
#include "VrEyeViewWidget.h"
|
|
|
|
#include <QMenuBar>
|
|
#include <QAction>
|
|
#include <QSplitter>
|
|
#include <QVBoxLayout>
|
|
#include <QHBoxLayout>
|
|
#include <QGridLayout>
|
|
#include <QGroupBox>
|
|
#include <QMessageBox>
|
|
#include <QFileDialog>
|
|
#include <QFile>
|
|
#include <QTextStream>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QJsonArray>
|
|
#include <QLabel>
|
|
#include <QScrollArea>
|
|
|
|
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<int>(HECEulerOrder::XYZ));
|
|
m_cbEulerOrder->addItem("XZY", static_cast<int>(HECEulerOrder::XZY));
|
|
m_cbEulerOrder->addItem("YXZ", static_cast<int>(HECEulerOrder::YXZ));
|
|
m_cbEulerOrder->addItem("YZX", static_cast<int>(HECEulerOrder::YZX));
|
|
m_cbEulerOrder->addItem("ZXY", static_cast<int>(HECEulerOrder::ZXY));
|
|
m_cbEulerOrder->addItem("ZYX (常用)", static_cast<int>(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<HECPoint3D> eyePoints;
|
|
std::vector<HECPoint3D> 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<HECEyeInHandData> 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<int>(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<HECEulerOrder>(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));
|
|
}
|