GrabBag/App/ScrewPosition/ScrewPositionApp/dialogdetectionconfig.cpp

498 lines
21 KiB
C++
Raw Normal View History

#include "dialogdetectionconfig.h"
#include "ui_dialogdetectionconfig.h"
#include <QComboBox>
#include <QDoubleValidator>
#include <QIntValidator>
#include <QLineEdit>
#include <QMessageBox>
#include <QPushButton>
#include <QVector>
#include <algorithm>
#include <cmath>
#include <cstring>
#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();
2026-06-04 09:36:25 +08:00
initNetworkConfigControls();
// 检测对象 ComboBox 选项(带枚举值作 userData
ui->comboType->blockSignals(true);
ui->comboType->clear();
ui->comboType->addItem(detectionTypeLabel(DETECTION_TYPE_SCREW),
static_cast<int>(DETECTION_TYPE_SCREW));
ui->comboType->addItem(detectionTypeLabel(DETECTION_TYPE_TOOL_DISK),
static_cast<int>(DETECTION_TYPE_TOOL_DISK));
ui->comboType->blockSignals(false);
connect(ui->comboCamera, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &DialogDetectionConfig::onSelectionChanged);
connect(ui->comboType, QOverload<int>::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();
2026-06-04 09:36:25 +08:00
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);
};
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);
}
2026-06-04 09:36:25 +08:00
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<int>(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<int>(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<HandEyeCalibCameraInfo> 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<int>(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<DetectionType>(ui->comboType->currentData().toInt());
return findWorkingIndex(camIdx, type);
}
void DialogDetectionConfig::loadWorkingSetToEditors(int workingIndex)
{
if (workingIndex < 0 || workingIndex >= static_cast<int>(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<int>(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);
}
2026-06-04 09:36:25 +08:00
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<uint16_t>(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 套配置已应用并写入 config.xml。"));
}
}
void DialogDetectionConfig::onResetCurrentClicked()
{
if (m_currentLoadedIndex < 0 || m_currentLoadedIndex >= static_cast<int>(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("螺杆");
}