#include "dialogdetectionconfig.h" #include "ui_dialogdetectionconfig.h" #include #include #include #include #include #include #include #include #include #include #include "HandEyeCalibWidget.h" #include "PathManager.h" #include "ScrewPositionPresenter.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(); // 检测对象 ComboBox 选项(带枚举值作 userData) ui->comboType->blockSignals(true); ui->comboType->clear(); ui->comboType->addItem(detectionTypeLabel(DETECTION_TYPE_SCREW), static_cast(DETECTION_TYPE_SCREW)); ui->comboType->addItem(detectionTypeLabel(DETECTION_TYPE_TOOL_DISK), static_cast(DETECTION_TYPE_TOOL_DISK)); 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(ScrewPositionPresenter* presenter) { m_presenter = presenter; loadFromConfig(); } 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); }; auto setupInt = [](QLineEdit* edit, int min, int max) { edit->setValidator(new QIntValidator(min, max, edit)); }; setupDouble(ui->spinRodDiameter, 1.0, 100.0, 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); setupDouble(ui->spinContinuityTh, 0.0, 100.0, 6); setupDouble(ui->spinOutlierTh, 0.0, 50.0, 6); setupInt (ui->spinMaxLineSkipNum, -1, 100); setupDouble(ui->spinYDeviationMax, 0.0, 100.0, 6); setupDouble(ui->spinMaxSkipDistance, 0.0, 100.0, 6); setupDouble(ui->spinZDeviationMax, 0.0, 100.0, 6); setupDouble(ui->spinMinLTypeTreeLen, 0.0, 1000.0, 6); setupDouble(ui->spinMinVTypeTreeLen, 0.0, 1000.0, 6); } void DialogDetectionConfig::initHandEyeCalibTab() { if (!ui->verticalLayout_handEyeCalibHost || m_handEyeCalibWidget) { return; } m_handEyeCalibWidget = new HandEyeCalibWidget(this); m_handEyeCalibWidget->setMatrixEditable(true); m_handEyeCalibWidget->setExtrinsicControlsVisible(true); m_handEyeCalibWidget->setApproachOffsetVisible(true); m_handEyeCalibWidget->setTargetOffsetVisible(true); m_handEyeCalibWidget->setDefaultFilePath(PathManager::GetInstance().GetAppConfigDirectory()); ui->verticalLayout_handEyeCalibHost->addWidget(m_handEyeCalibWidget); } 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_SCREW, DETECTION_TYPE_TOOL_DISK }; 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 配置成「单条目」模式:只展示当前选中那一份。 // 之所以不让它显示 4 条目,是因为本对话框已经在外部提供 (相机, 对象) 选择器, // 内嵌相机选择器会让用户不知道编辑的是 screw 还是 toolDisk。 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)); m_handEyeCalibWidget->setExtrinsicData(1, ws.handEyeMatrix.eulerOrder, ws.handEyeMatrix.rotX, ws.handEyeMatrix.rotY, ws.handEyeMatrix.rotZ, ws.handEyeMatrix.approachOffset, ws.handEyeMatrix.offsetX, ws.handEyeMatrix.offsetY, ws.handEyeMatrix.offsetZ); const auto& algo = ws.algorithmParams; setDoubleEditorText(ui->spinRodDiameter, algo.screwParam.rodDiameter); ui->chkHorizonScan->setChecked(algo.screwParam.isHorizonScan); setDoubleEditorText(ui->spinCornerTh, algo.cornerParam.cornerTh); setDoubleEditorText(ui->spinScale, algo.cornerParam.scale); setDoubleEditorText(ui->spinMinEndingGap, algo.cornerParam.minEndingGap); setDoubleEditorText(ui->spinMinEndingGapZ, algo.cornerParam.minEndingGap_z); setDoubleEditorText(ui->spinJumpCornerTh1, algo.cornerParam.jumpCornerTh_1); setDoubleEditorText(ui->spinJumpCornerTh2, algo.cornerParam.jumpCornerTh_2); setDoubleEditorText(ui->spinContinuityTh, algo.filterParam.continuityTh); setDoubleEditorText(ui->spinOutlierTh, algo.filterParam.outlierTh); setIntEditorText (ui->spinMaxLineSkipNum, algo.growParam.maxLineSkipNum); setDoubleEditorText(ui->spinYDeviationMax, algo.growParam.yDeviation_max); setDoubleEditorText(ui->spinMaxSkipDistance, algo.growParam.maxSkipDistance); setDoubleEditorText(ui->spinZDeviationMax, algo.growParam.zDeviation_max); setDoubleEditorText(ui->spinMinLTypeTreeLen, algo.growParam.minLTypeTreeLen); setDoubleEditorText(ui->spinMinVTypeTreeLen, algo.growParam.minVTypeTreeLen); 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)); } int eulerOrder = ws.handEyeMatrix.eulerOrder; double rotX = ws.handEyeMatrix.rotX, rotY = ws.handEyeMatrix.rotY, rotZ = ws.handEyeMatrix.rotZ; double approachOffset = ws.handEyeMatrix.approachOffset; double offX = ws.handEyeMatrix.offsetX, offY = ws.handEyeMatrix.offsetY, offZ = ws.handEyeMatrix.offsetZ; if (m_handEyeCalibWidget->getExtrinsicData(1, eulerOrder, rotX, rotY, rotZ, approachOffset, offX, offY, offZ)) { ws.handEyeMatrix.eulerOrder = eulerOrder; ws.handEyeMatrix.rotX = rotX; ws.handEyeMatrix.rotY = rotY; ws.handEyeMatrix.rotZ = rotZ; ws.handEyeMatrix.approachOffset = approachOffset; ws.handEyeMatrix.offsetX = offX; ws.handEyeMatrix.offsetY = offY; ws.handEyeMatrix.offsetZ = offZ; } ws.handEyeMatrix.cameraIndex = ws.cameraIndex; } auto fail = [&](const QString& m) { if (errMsg) *errMsg = m; return false; }; auto& algo = ws.algorithmParams; if (!tryGetDouble(ui->spinRodDiameter, QStringLiteral("螺杆直径"), algo.screwParam.rodDiameter)) return fail(QStringLiteral("螺杆直径")); algo.screwParam.isHorizonScan = ui->chkHorizonScan->isChecked(); if (!tryGetDouble(ui->spinCornerTh, QStringLiteral("拐角阈值"), algo.cornerParam.cornerTh)) return fail(QStringLiteral("拐角阈值")); if (!tryGetDouble(ui->spinScale, QStringLiteral("窗口比例因子"), algo.cornerParam.scale)) return fail(QStringLiteral("窗口比例因子")); if (!tryGetDouble(ui->spinMinEndingGap, QStringLiteral("Y方向最小结束间隙"), algo.cornerParam.minEndingGap)) return fail(QStringLiteral("Y方向最小结束间隙")); if (!tryGetDouble(ui->spinMinEndingGapZ, QStringLiteral("Z方向最小结束间隙"), algo.cornerParam.minEndingGap_z)) return fail(QStringLiteral("Z方向最小结束间隙")); if (!tryGetDouble(ui->spinJumpCornerTh1, QStringLiteral("跳变拐角阈值1"), algo.cornerParam.jumpCornerTh_1)) return fail(QStringLiteral("跳变拐角阈值1")); if (!tryGetDouble(ui->spinJumpCornerTh2, QStringLiteral("跳变拐角阈值2"), algo.cornerParam.jumpCornerTh_2)) return fail(QStringLiteral("跳变拐角阈值2")); if (!tryGetDouble(ui->spinContinuityTh, QStringLiteral("连续性阈值"), algo.filterParam.continuityTh)) return fail(QStringLiteral("连续性阈值")); if (!tryGetDouble(ui->spinOutlierTh, QStringLiteral("离群点阈值"), algo.filterParam.outlierTh)) return fail(QStringLiteral("离群点阈值")); if (!tryGetInt (ui->spinMaxLineSkipNum, QStringLiteral("最大跳线数"), algo.growParam.maxLineSkipNum)) return fail(QStringLiteral("最大跳线数")); if (!tryGetDouble(ui->spinYDeviationMax, QStringLiteral("Y偏差最大值"), algo.growParam.yDeviation_max)) return fail(QStringLiteral("Y偏差最大值")); if (!tryGetDouble(ui->spinMaxSkipDistance, QStringLiteral("最大跳跃距离"), algo.growParam.maxSkipDistance)) return fail(QStringLiteral("最大跳跃距离")); if (!tryGetDouble(ui->spinZDeviationMax, QStringLiteral("Z偏差最大值"), algo.growParam.zDeviation_max)) return fail(QStringLiteral("Z偏差最大值")); if (!tryGetDouble(ui->spinMinLTypeTreeLen, QStringLiteral("L型树最小长度"), algo.growParam.minLTypeTreeLen)) return fail(QStringLiteral("L型树最小长度")); if (!tryGetDouble(ui->spinMinVTypeTreeLen, QStringLiteral("V型树最小长度"), algo.growParam.minVTypeTreeLen)) return fail(QStringLiteral("V型树最小长度")); 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); } 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 套配置已应用并写入 config.xml。")); } } 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_TOOL_DISK) ? QStringLiteral("工具盘") : QStringLiteral("螺杆"); }