#include "dialogdetectionconfig.h" #include "ui_dialogdetectionconfig.h" #include #include #include #include #include #include #include #include #include #include #include "HandEyeCalibWidget.h" #include "ToolExtrinsicWidget.h" #include "PathManager.h" #include "DiscHolePosePresenter.h" #include "StyledMessageBox.h" namespace { bool IsIdentityMatrix(const double matrix[16]) { for (int i = 0; i < 16; ++i) { const double expected = (i / 4 == i % 4) ? 1.0 : 0.0; if (std::fabs(matrix[i] - expected) > 1e-9) { return false; } } return true; } } // namespace DialogDetectionConfig::DialogDetectionConfig(QWidget *parent) : QDialog(parent) , ui(new Ui::DialogDetectionConfig) { ui->setupUi(this); setWindowTitle(QStringLiteral("砂轮盘孔定位 - 检测对象配置")); initNumericEditors(); initHandEyeCalibTab(); initToolParamsTab(); initNetworkConfigControls(); // 检测对象 ComboBox 选项(DiscHolePose: 砂轮盘孔 / 砂轮盘架子) ui->comboType->blockSignals(true); ui->comboType->clear(); ui->comboType->addItem(detectionTypeLabel(DETECTION_TYPE_DISC_HOLE), static_cast(DETECTION_TYPE_DISC_HOLE)); ui->comboType->addItem(detectionTypeLabel(DETECTION_TYPE_DISC_RACK), static_cast(DETECTION_TYPE_DISC_RACK)); ui->comboType->blockSignals(false); connect(ui->comboCamera, QOverload::of(&QComboBox::currentIndexChanged), this, &DialogDetectionConfig::onSelectionChanged); connect(ui->comboType, QOverload::of(&QComboBox::currentIndexChanged), this, &DialogDetectionConfig::onSelectionChanged); connect(ui->btnOk, &QPushButton::clicked, this, &DialogDetectionConfig::onOkClicked); connect(ui->btnCancel, &QPushButton::clicked, this, &DialogDetectionConfig::onCancelClicked); connect(ui->btnApply, &QPushButton::clicked, this, &DialogDetectionConfig::onApplyClicked); connect(ui->btnReset, &QPushButton::clicked, this, &DialogDetectionConfig::onResetCurrentClicked); } DialogDetectionConfig::~DialogDetectionConfig() { delete ui; } void DialogDetectionConfig::SetPresenter(DiscHolePosePresenter* presenter) { m_presenter = presenter; loadFromConfig(); loadNetworkConfig(); } void DialogDetectionConfig::initNumericEditors() { auto setupDouble = [](QLineEdit* edit, double min, double max, int decimals) { auto* validator = new QDoubleValidator(min, max, decimals, edit); validator->setNotation(QDoubleValidator::StandardNotation); edit->setValidator(validator); }; // DiscHolePose 仅使用角点检测参数(6 个) setupDouble(ui->spinCornerTh, 0.0, 180.0, 6); setupDouble(ui->spinScale, 0.0, 200.0, 6); setupDouble(ui->spinMinEndingGap, 0.0, 100.0, 6); setupDouble(ui->spinMinEndingGapZ, 0.0, 100.0, 6); setupDouble(ui->spinJumpCornerTh1, 0.0, 100.0, 6); setupDouble(ui->spinJumpCornerTh2, 0.0, 180.0, 6); } void DialogDetectionConfig::initHandEyeCalibTab() { if (!ui->verticalLayout_handEyeCalibHost || m_handEyeCalibWidget) { return; } m_handEyeCalibWidget = new HandEyeCalibWidget(this); m_handEyeCalibWidget->setMatrixEditable(true); m_handEyeCalibWidget->setExtrinsicControlsVisible(false); m_handEyeCalibWidget->setTargetOffsetVisible(false); m_handEyeCalibWidget->setDefaultFilePath(PathManager::GetInstance().GetAppConfigDirectory()); ui->verticalLayout_handEyeCalibHost->addWidget(m_handEyeCalibWidget); } void DialogDetectionConfig::initToolParamsTab() { if (!ui->verticalLayout_toolParamsHost || m_toolParamsWidget) { return; } m_toolParamsWidget = new ToolExtrinsicWidget(this); m_toolParamsWidget->setTargetOffsetVisible(true); ui->verticalLayout_toolParamsHost->addWidget(m_toolParamsWidget); } void DialogDetectionConfig::initNetworkConfigControls() { ui->comboPoseOutputOrder->clear(); ui->comboPoseOutputOrder->addItem(QStringLiteral("RX-RY-RZ"), 0); ui->comboPoseOutputOrder->addItem(QStringLiteral("RX-RZ-RY"), 1); ui->comboPoseOutputOrder->addItem(QStringLiteral("RY-RX-RZ"), 2); ui->comboPoseOutputOrder->addItem(QStringLiteral("RY-RZ-RX"), 3); ui->comboPoseOutputOrder->addItem(QStringLiteral("RZ-RX-RY"), 4); ui->comboPoseOutputOrder->addItem(QStringLiteral("RZ-RY-RX"), 5); ui->comboByteOrder->clear(); ui->comboByteOrder->addItem(QStringLiteral("大端序 (ABCD)"), 0); ui->comboByteOrder->addItem(QStringLiteral("小端序 (DCBA)"), 1); ui->editTcpPort->setValidator(new QIntValidator(1, 65535, ui->editTcpPort)); } void DialogDetectionConfig::loadNetworkConfig() { if (!m_presenter || !m_presenter->GetConfigManager()) { return; } const ConfigResult configResult = m_presenter->GetConfigManager()->GetConfigResult(); int idx = ui->comboPoseOutputOrder->findData(configResult.poseOutputOrder); if (idx >= 0) { ui->comboPoseOutputOrder->setCurrentIndex(idx); } idx = ui->comboByteOrder->findData(configResult.byteOrder); if (idx >= 0) { ui->comboByteOrder->setCurrentIndex(idx); } ui->editTcpPort->setText(QString::number(configResult.tcpPort)); } void DialogDetectionConfig::rebuildCameraSelector() { if (!ui->comboCamera) return; ui->comboCamera->blockSignals(true); ui->comboCamera->clear(); for (int camIdx = 1; camIdx <= m_cameraCount; ++camIdx) { QString name = QStringLiteral("相机 %1").arg(camIdx); if (m_presenter && m_presenter->GetConfigManager()) { const auto& cameras = m_presenter->GetConfigManager()->GetConfigResult().cameraList; const int arrayIndex = camIdx - 1; if (arrayIndex >= 0 && arrayIndex < static_cast(cameras.size())) { name = QString::fromStdString(cameras[arrayIndex].name); } } ui->comboCamera->addItem(name, camIdx); } ui->comboCamera->blockSignals(false); } void DialogDetectionConfig::loadFromConfig() { if (!m_presenter || !m_presenter->GetConfigManager()) { return; } const ConfigResult configResult = m_presenter->GetConfigManager()->GetConfigResult(); m_cameraCount = std::max(1, static_cast(configResult.cameraList.size())); // 把当前 ConfigResult 中所有 (相机, 对象) set 抓到 m_workingSets, // 缺失的槽位用默认值兜底补齐。 m_workingSets.clear(); const DetectionType allTypes[2] = { DETECTION_TYPE_DISC_HOLE, DETECTION_TYPE_DISC_RACK }; for (int camIdx = 1; camIdx <= m_cameraCount; ++camIdx) { for (DetectionType t : allTypes) { WorkingSet ws; ws.cameraIndex = camIdx; ws.detectionType = t; ws.handEyeMatrix.cameraIndex = camIdx; if (const auto* item = configResult.FindDetectionConfig(camIdx, t)) { ws.handEyeMatrix = item->handEyeMatrix; ws.handEyeMatrix.cameraIndex = camIdx; ws.algorithmParams = item->algorithmParams; } m_workingSets.push_back(ws); } } // 把 HandEyeCalibWidget 配置成「单条目」模式:只展示当前选中那一份。 QVector singleCam; HandEyeCalibCameraInfo info; info.cameraIndex = 1; info.displayName = QStringLiteral("当前"); singleCam.append(info); m_handEyeCalibWidget->setCameraList(singleCam); rebuildCameraSelector(); // 默认选中 (相机 1, 砂轮盘孔) ui->comboCamera->setCurrentIndex(0); ui->comboType->setCurrentIndex(0); m_currentLoadedIndex = -1; loadWorkingSetToEditors(currentWorkingIndex()); } int DialogDetectionConfig::findWorkingIndex(int cameraIndex, DetectionType type) const { for (size_t i = 0; i < m_workingSets.size(); ++i) { if (m_workingSets[i].cameraIndex == cameraIndex && m_workingSets[i].detectionType == type) { return static_cast(i); } } return -1; } int DialogDetectionConfig::currentWorkingIndex() const { if (!ui || !ui->comboCamera || !ui->comboType) return -1; const int camIdx = ui->comboCamera->currentData().toInt(); const auto type = static_cast(ui->comboType->currentData().toInt()); return findWorkingIndex(camIdx, type); } void DialogDetectionConfig::loadWorkingSetToEditors(int workingIndex) { if (workingIndex < 0 || workingIndex >= static_cast(m_workingSets.size())) { return; } const auto& ws = m_workingSets[workingIndex]; // 手眼控件单条目模式:始终用虚拟 cameraIndex=1 m_handEyeCalibWidget->setCalibData(1, ws.handEyeMatrix.matrix, !IsIdentityMatrix(ws.handEyeMatrix.matrix)); // 工具参数 if (m_toolParamsWidget) { m_toolParamsWidget->setData( ws.handEyeMatrix.eulerOrder, ws.handEyeMatrix.rotX, ws.handEyeMatrix.rotY, ws.handEyeMatrix.rotZ, ws.handEyeMatrix.offsetX, ws.handEyeMatrix.offsetY, ws.handEyeMatrix.offsetZ); } // DiscHolePose 仅加载角点检测参数 const auto& corner = ws.algorithmParams.cornerParam; setDoubleEditorText(ui->spinCornerTh, corner.cornerTh); setDoubleEditorText(ui->spinScale, corner.scale); setDoubleEditorText(ui->spinMinEndingGap, corner.minEndingGap); setDoubleEditorText(ui->spinMinEndingGapZ, corner.minEndingGap_z); setDoubleEditorText(ui->spinJumpCornerTh1, corner.jumpCornerTh_1); setDoubleEditorText(ui->spinJumpCornerTh2, corner.jumpCornerTh_2); m_currentLoadedIndex = workingIndex; } bool DialogDetectionConfig::commitCurrentEditorsToWorkingSet(QString* errMsg) { if (m_currentLoadedIndex < 0 || m_currentLoadedIndex >= static_cast(m_workingSets.size())) { return true; } auto& ws = m_workingSets[m_currentLoadedIndex]; // 手眼控件(矩阵,虚拟 cameraIndex=1) if (m_handEyeCalibWidget) { double matrix[16]; bool isCalibrated = false; if (m_handEyeCalibWidget->getCalibData(1, matrix, isCalibrated)) { std::memcpy(ws.handEyeMatrix.matrix, matrix, sizeof(matrix)); } } // 工具参数 if (m_toolParamsWidget) { m_toolParamsWidget->getData( ws.handEyeMatrix.eulerOrder, ws.handEyeMatrix.rotX, ws.handEyeMatrix.rotY, ws.handEyeMatrix.rotZ, ws.handEyeMatrix.offsetX, ws.handEyeMatrix.offsetY, ws.handEyeMatrix.offsetZ); } ws.handEyeMatrix.cameraIndex = ws.cameraIndex; auto fail = [&](const QString& m) { if (errMsg) *errMsg = m; return false; }; auto& corner = ws.algorithmParams.cornerParam; // DiscHolePose 仅读取角点检测参数 if (!tryGetDouble(ui->spinCornerTh, QStringLiteral("拐角阈值"), corner.cornerTh)) return fail(QStringLiteral("拐角阈值")); if (!tryGetDouble(ui->spinScale, QStringLiteral("窗口比例因子"), corner.scale)) return fail(QStringLiteral("窗口比例因子")); if (!tryGetDouble(ui->spinMinEndingGap, QStringLiteral("Y方向最小结束间隙"), corner.minEndingGap)) return fail(QStringLiteral("Y方向最小结束间隙")); if (!tryGetDouble(ui->spinMinEndingGapZ, QStringLiteral("Z方向最小结束间隙"), corner.minEndingGap_z)) return fail(QStringLiteral("Z方向最小结束间隙")); if (!tryGetDouble(ui->spinJumpCornerTh1, QStringLiteral("跳变拐角阈值1"), corner.jumpCornerTh_1)) return fail(QStringLiteral("跳变拐角阈值1")); if (!tryGetDouble(ui->spinJumpCornerTh2, QStringLiteral("跳变拐角阈值2"), corner.jumpCornerTh_2)) return fail(QStringLiteral("跳变拐角阈值2")); return true; } bool DialogDetectionConfig::saveAllWorkingSetsToConfig() { if (!m_presenter || !m_presenter->GetConfigManager()) { StyledMessageBox::warning(this, QStringLiteral("失败"), QStringLiteral("ConfigManager 未初始化。")); return false; } QString err; if (!commitCurrentEditorsToWorkingSet(&err)) { // tryGetDouble/tryGetInt 内部已经弹了具体错误框;这里不再重复 return false; } SystemConfig systemConfig = m_presenter->GetConfigManager()->GetConfig(); // 用 m_workingSets 全量覆盖 detectionConfigList systemConfig.configResult.detectionConfigList.clear(); for (const auto& ws : m_workingSets) { VrDetectionConfigSet item; item.cameraIndex = ws.cameraIndex; item.detectionType = ws.detectionType; item.handEyeMatrix = ws.handEyeMatrix; item.handEyeMatrix.cameraIndex = ws.cameraIndex; item.algorithmParams = ws.algorithmParams; systemConfig.configResult.detectionConfigList.push_back(item); } bool ok = false; const int tcpPort = ui->editTcpPort->text().trimmed().toInt(&ok); if (!ok || tcpPort <= 0 || tcpPort > 65535) { StyledMessageBox::warning(this, QStringLiteral("错误"), QStringLiteral("TCP端口必须在 1 到 65535 之间。")); ui->editTcpPort->setFocus(); ui->editTcpPort->selectAll(); return false; } systemConfig.configResult.tcpPort = static_cast(tcpPort); systemConfig.configResult.poseOutputOrder = ui->comboPoseOutputOrder->currentData().toInt(); systemConfig.configResult.byteOrder = ui->comboByteOrder->currentData().toInt(); if (!m_presenter->GetConfigManager()->UpdateFullConfig(systemConfig)) { StyledMessageBox::warning(this, QStringLiteral("失败"), QStringLiteral("更新配置缓存失败。")); return false; } const QString configPath = PathManager::GetInstance().GetConfigFilePath(); if (!m_presenter->GetConfigManager()->SaveConfigToFile(configPath.toStdString())) { StyledMessageBox::warning(this, QStringLiteral("失败"), QStringLiteral("保存配置文件失败。")); return false; } m_presenter->OnConfigChanged(systemConfig.configResult); return true; } // —— 槽函数 —— void DialogDetectionConfig::onSelectionChanged() { // 切换前先提交当前编辑器内容到旧 set;失败则回滚 combo 选择。 QString err; if (!commitCurrentEditorsToWorkingSet(&err)) { // 用户编辑了非法值,重新加载旧 set,避免进入未定义状态 loadWorkingSetToEditors(m_currentLoadedIndex); return; } const int newIndex = currentWorkingIndex(); loadWorkingSetToEditors(newIndex); } void DialogDetectionConfig::onOkClicked() { if (saveAllWorkingSetsToConfig()) { accept(); } } void DialogDetectionConfig::onCancelClicked() { reject(); } void DialogDetectionConfig::onApplyClicked() { if (saveAllWorkingSetsToConfig()) { StyledMessageBox::information(this, QStringLiteral("提示"), QStringLiteral("4 套配置已应用并写入配置文件。")); } } void DialogDetectionConfig::onResetCurrentClicked() { if (m_currentLoadedIndex < 0 || m_currentLoadedIndex >= static_cast(m_workingSets.size())) { return; } auto ret = StyledMessageBox::question(this, QStringLiteral("确认重置"), QStringLiteral("确定要把当前 (相机, 对象) 配置重置为默认值吗?\n(仅影响内存中正在编辑的副本,确定/应用前不会落盘)"), QMessageBox::Yes | QMessageBox::No); if (ret != QMessageBox::Yes) return; WorkingSet defaults; defaults.cameraIndex = m_workingSets[m_currentLoadedIndex].cameraIndex; defaults.detectionType = m_workingSets[m_currentLoadedIndex].detectionType; defaults.handEyeMatrix.cameraIndex = defaults.cameraIndex; m_workingSets[m_currentLoadedIndex] = defaults; loadWorkingSetToEditors(m_currentLoadedIndex); } // —— 工具函数 —— void DialogDetectionConfig::setDoubleEditorText(QLineEdit* edit, double value) { if (edit) edit->setText(QString::number(value, 'g', 12)); } void DialogDetectionConfig::setIntEditorText(QLineEdit* edit, int value) { if (edit) edit->setText(QString::number(value)); } bool DialogDetectionConfig::tryGetDouble(QLineEdit* edit, const QString& label, double& value) { bool ok = false; value = edit->text().trimmed().toDouble(&ok); if (!ok) { StyledMessageBox::warning(this, QStringLiteral("错误"), QStringLiteral("\"%1\" 输入的数值无效。").arg(label)); edit->setFocus(); edit->selectAll(); return false; } return true; } bool DialogDetectionConfig::tryGetInt(QLineEdit* edit, const QString& label, int& value) { bool ok = false; value = edit->text().trimmed().toInt(&ok); if (!ok) { StyledMessageBox::warning(this, QStringLiteral("错误"), QStringLiteral("\"%1\" 输入的整数无效。").arg(label)); edit->setFocus(); edit->selectAll(); return false; } return true; } QString DialogDetectionConfig::detectionTypeLabel(DetectionType type) { return (type == DETECTION_TYPE_DISC_RACK) ? QStringLiteral("砂轮盘架子(discRack)") : QStringLiteral("砂轮盘孔(discHole)"); }