螺栓检测结果列表更新
This commit is contained in:
parent
81a5298e14
commit
a6d3bd240d
@ -41,6 +41,7 @@ SOURCES += \
|
||||
mainwindow.cpp \
|
||||
dialogalgoarg.cpp \
|
||||
dialogcamerasetting.cpp \
|
||||
dialoghistoryresult.cpp \
|
||||
resultitem.cpp \
|
||||
Presenter/Src/ConfigManager.cpp \
|
||||
Presenter/Src/DroneScrewCtrlPresenter.cpp
|
||||
@ -49,6 +50,7 @@ HEADERS += \
|
||||
mainwindow.h \
|
||||
dialogalgoarg.h \
|
||||
dialogcamerasetting.h \
|
||||
dialoghistoryresult.h \
|
||||
resultitem.h \
|
||||
Version.h \
|
||||
IYDroneScrewCtrlStatus.h \
|
||||
@ -59,6 +61,7 @@ FORMS += \
|
||||
mainwindow.ui \
|
||||
dialogalgoarg.ui \
|
||||
dialogcamerasetting.ui \
|
||||
dialoghistoryresult.ui \
|
||||
resultitem.ui
|
||||
|
||||
RESOURCES += \
|
||||
|
||||
@ -19,6 +19,13 @@ struct CtrlDetectionBox
|
||||
int height{0};
|
||||
bool hasPhysicalHeight{false};
|
||||
float physicalHeightMm{0.0f};
|
||||
bool hasStereoMatch{false};
|
||||
bool trusted{false};
|
||||
int matchConfidence{1};
|
||||
QString matchStatus;
|
||||
int pairId{-1};
|
||||
int leftIndex{-1};
|
||||
int rightIndex{-1};
|
||||
};
|
||||
|
||||
/**
|
||||
@ -43,6 +50,10 @@ struct CtrlDetectionFrame
|
||||
bool success{true};
|
||||
int errorCode{0};
|
||||
QString message;
|
||||
bool isFinalResult{false};
|
||||
double rankScore{0.0};
|
||||
int rankSampleCount{0};
|
||||
qint64 saveIndex{0};
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(CtrlDetectionFrame)
|
||||
|
||||
@ -53,6 +53,7 @@ public:
|
||||
bool SetServerExposure(double exposureTime);
|
||||
bool SetServerGain(double gain);
|
||||
bool PushAlgoParams(const DroneScrewAlgoUiParams& params);
|
||||
bool QueryServerInfo(WDRemoteServerInfo& info, int timeoutMs = 3000);
|
||||
|
||||
QImage GetLastDisplayImage() const;
|
||||
|
||||
|
||||
@ -577,10 +577,24 @@ bool DroneScrewCtrlPresenter::PushAlgoParams(const DroneScrewAlgoUiParams& param
|
||||
p.inputWidth = params.inputWidth;
|
||||
p.inputHeight = params.inputHeight;
|
||||
p.modelType = params.modelType;
|
||||
p.modelPath = params.modelPath;
|
||||
p.expectedBoltCount = params.expectedBoltCount;
|
||||
return m_pRemote->SetAlgoParams(p) == 0;
|
||||
}
|
||||
|
||||
bool DroneScrewCtrlPresenter::QueryServerInfo(WDRemoteServerInfo& info, int timeoutMs)
|
||||
{
|
||||
info = WDRemoteServerInfo{};
|
||||
if (!m_pRemote || !m_bConnected.load())
|
||||
return false;
|
||||
|
||||
info = m_pRemote->GetServerInfo(timeoutMs);
|
||||
if (!info.ok)
|
||||
return false;
|
||||
|
||||
updateStreamInfo(info);
|
||||
return true;
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// WDRemoteReceiver 回调
|
||||
// =====================================================================
|
||||
@ -748,6 +762,10 @@ CtrlDetectionFrame DroneScrewCtrlPresenter::toCtrlFrame(const WDRemoteDetectionF
|
||||
frame.success = f.success;
|
||||
frame.errorCode = f.errorCode;
|
||||
frame.message = QString::fromStdString(f.message);
|
||||
frame.isFinalResult = f.isFinalResult;
|
||||
frame.rankScore = f.rankScore;
|
||||
frame.rankSampleCount = f.rankSampleCount;
|
||||
frame.saveIndex = static_cast<qint64>(f.saveIndex);
|
||||
frame.boxes.reserve(f.boxes.size());
|
||||
for (const auto& b : f.boxes)
|
||||
{
|
||||
@ -760,6 +778,13 @@ CtrlDetectionFrame DroneScrewCtrlPresenter::toCtrlFrame(const WDRemoteDetectionF
|
||||
cb.height = b.height;
|
||||
cb.hasPhysicalHeight = b.hasPhysicalHeight;
|
||||
cb.physicalHeightMm = b.physicalHeightMm;
|
||||
cb.hasStereoMatch = b.hasStereoMatch;
|
||||
cb.trusted = b.trusted;
|
||||
cb.matchConfidence = b.matchConfidence;
|
||||
cb.matchStatus = QString::fromStdString(b.matchStatus);
|
||||
cb.pairId = b.pairId;
|
||||
cb.leftIndex = b.leftIndex;
|
||||
cb.rightIndex = b.rightIndex;
|
||||
frame.boxes.push_back(cb);
|
||||
}
|
||||
frame.distances.reserve(f.distances.size());
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
|
||||
#define DRONESCREWCTRL_APP_NAME "无人机螺杆控制端"
|
||||
#define DRONESCREWCTRL_VERSION_STRING "1.0.0"
|
||||
#define DRONESCREWCTRL_BUILD_STRING "4"
|
||||
#define DRONESCREWCTRL_FULL_VERSION_STRING "V1.0.0_4"
|
||||
#define DRONESCREWCTRL_BUILD_STRING "5"
|
||||
#define DRONESCREWCTRL_FULL_VERSION_STRING "V1.0.0_5"
|
||||
|
||||
inline const char* GetDroneScrewCtrlFullVersion() { return DRONESCREWCTRL_FULL_VERSION_STRING; }
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
inputWidth="640"
|
||||
inputHeight="640"
|
||||
modelType="0"
|
||||
modelPath="" />
|
||||
expectedBoltCount="8" />
|
||||
|
||||
<!-- 显示选项 -->
|
||||
<Display drawBoxes="true"
|
||||
|
||||
@ -3,9 +3,45 @@
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QDoubleValidator>
|
||||
#include <QEvent>
|
||||
#include <QFormLayout>
|
||||
#include <QGuiApplication>
|
||||
#include <QInputMethod>
|
||||
#include <QIntValidator>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QTimer>
|
||||
|
||||
namespace
|
||||
{
|
||||
void SetupKeyboardEdit(QLineEdit* edit, QObject* filter, Qt::InputMethodHints hints)
|
||||
{
|
||||
if (!edit)
|
||||
return;
|
||||
|
||||
edit->setAttribute(Qt::WA_InputMethodEnabled, true);
|
||||
edit->setInputMethodHints(hints);
|
||||
edit->installEventFilter(filter);
|
||||
}
|
||||
|
||||
void ShowSystemKeyboard(QWidget* focusWidget)
|
||||
{
|
||||
if (!focusWidget)
|
||||
return;
|
||||
|
||||
focusWidget->setFocus(Qt::MouseFocusReason);
|
||||
|
||||
QInputMethod* inputMethod = QGuiApplication::inputMethod();
|
||||
if (inputMethod)
|
||||
inputMethod->show();
|
||||
|
||||
QTimer::singleShot(0, focusWidget, []() {
|
||||
QInputMethod* delayedInputMethod = QGuiApplication::inputMethod();
|
||||
if (delayedInputMethod)
|
||||
delayedInputMethod->show();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
DialogAlgoArg::DialogAlgoArg(QWidget* parent)
|
||||
: QDialog(parent)
|
||||
@ -20,6 +56,8 @@ DialogAlgoArg::DialogAlgoArg(QWidget* parent)
|
||||
ui->edit_width->setValidator(new QIntValidator(0, 8192, this));
|
||||
ui->edit_height->setValidator(new QIntValidator(0, 8192, this));
|
||||
ui->edit_modelType->setValidator(new QIntValidator(0, 10, this));
|
||||
ui->edit_expectedBoltCount->setValidator(new QIntValidator(1, 64, this));
|
||||
setupInputKeyboard();
|
||||
createDisplayRows();
|
||||
}
|
||||
|
||||
@ -57,7 +95,7 @@ void DialogAlgoArg::loadToUi(const DroneScrewAlgoUiParams& p)
|
||||
if (ui->edit_width) ui->edit_width->setText(QString::number(p.inputWidth));
|
||||
if (ui->edit_height) ui->edit_height->setText(QString::number(p.inputHeight));
|
||||
if (ui->edit_modelType) ui->edit_modelType->setText(QString::number(p.modelType));
|
||||
if (ui->edit_model) ui->edit_model->setText(QString::fromStdString(p.modelPath));
|
||||
if (ui->edit_expectedBoltCount) ui->edit_expectedBoltCount->setText(QString::number(p.expectedBoltCount));
|
||||
}
|
||||
|
||||
DroneScrewAlgoUiParams DialogAlgoArg::collectFromUi() const
|
||||
@ -68,10 +106,46 @@ DroneScrewAlgoUiParams DialogAlgoArg::collectFromUi() const
|
||||
if (ui->edit_width) { bool ok; int v = ui->edit_width->text().toInt(&ok); if (ok) p.inputWidth = v; }
|
||||
if (ui->edit_height) { bool ok; int v = ui->edit_height->text().toInt(&ok); if (ok) p.inputHeight = v; }
|
||||
if (ui->edit_modelType) { bool ok; int v = ui->edit_modelType->text().toInt(&ok); if (ok) p.modelType = v; }
|
||||
if (ui->edit_model) p.modelPath = ui->edit_model->text().toStdString();
|
||||
if (ui->edit_expectedBoltCount) { bool ok; int v = ui->edit_expectedBoltCount->text().toInt(&ok); if (ok) p.expectedBoltCount = v; }
|
||||
return p;
|
||||
}
|
||||
|
||||
bool DialogAlgoArg::eventFilter(QObject* watched, QEvent* event)
|
||||
{
|
||||
QLineEdit* edit = qobject_cast<QLineEdit*>(watched);
|
||||
if (edit)
|
||||
{
|
||||
switch (event->type())
|
||||
{
|
||||
case QEvent::FocusIn:
|
||||
case QEvent::MouseButtonPress:
|
||||
case QEvent::MouseButtonRelease:
|
||||
case QEvent::TouchEnd:
|
||||
ShowSystemKeyboard(edit);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return QDialog::eventFilter(watched, event);
|
||||
}
|
||||
|
||||
void DialogAlgoArg::setupInputKeyboard()
|
||||
{
|
||||
const Qt::InputMethodHints numberHints =
|
||||
Qt::ImhPreferNumbers | Qt::ImhFormattedNumbersOnly;
|
||||
const Qt::InputMethodHints integerHints =
|
||||
Qt::ImhPreferNumbers | Qt::ImhDigitsOnly;
|
||||
|
||||
SetupKeyboardEdit(ui->edit_score, this, numberHints);
|
||||
SetupKeyboardEdit(ui->edit_nms, this, numberHints);
|
||||
SetupKeyboardEdit(ui->edit_width, this, integerHints);
|
||||
SetupKeyboardEdit(ui->edit_height, this, integerHints);
|
||||
SetupKeyboardEdit(ui->edit_modelType, this, integerHints);
|
||||
SetupKeyboardEdit(ui->edit_expectedBoltCount, this, integerHints);
|
||||
}
|
||||
|
||||
void DialogAlgoArg::createDisplayRows()
|
||||
{
|
||||
if (!ui->fl_main)
|
||||
|
||||
@ -27,8 +27,10 @@ private slots:
|
||||
void on_btn_reset_clicked();
|
||||
|
||||
private:
|
||||
bool eventFilter(QObject* watched, QEvent* event) override;
|
||||
void loadToUi(const DroneScrewAlgoUiParams& p);
|
||||
DroneScrewAlgoUiParams collectFromUi() const;
|
||||
void setupInputKeyboard();
|
||||
void createDisplayRows();
|
||||
void loadDisplayToUi(const DroneScrewDisplayOption& options);
|
||||
DroneScrewDisplayOption collectDisplayFromUi() const;
|
||||
|
||||
@ -74,11 +74,12 @@ QLineEdit { color: rgb(239, 241, 245); background-color: rgb(47, 48, 52); border
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="lb_model"><property name="text"><string>模型路径</string></property></widget>
|
||||
<widget class="QLabel" name="lb_expectedBoltCount"><property name="text"><string>期望个数</string></property></widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<widget class="QLineEdit" name="edit_model">
|
||||
<widget class="QLineEdit" name="edit_expectedBoltCount">
|
||||
<property name="minimumSize"><size><width>0</width><height>40</height></size></property>
|
||||
<property name="placeholderText"><string>1 ~ 64</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
|
||||
@ -2,6 +2,42 @@
|
||||
#include "ui_dialogcamerasetting.h"
|
||||
|
||||
#include <QDoubleValidator>
|
||||
#include <QEvent>
|
||||
#include <QGuiApplication>
|
||||
#include <QInputMethod>
|
||||
#include <QLineEdit>
|
||||
#include <QTimer>
|
||||
|
||||
namespace
|
||||
{
|
||||
void SetupKeyboardEdit(QLineEdit* edit, QObject* filter, Qt::InputMethodHints hints)
|
||||
{
|
||||
if (!edit)
|
||||
return;
|
||||
|
||||
edit->setAttribute(Qt::WA_InputMethodEnabled, true);
|
||||
edit->setInputMethodHints(hints);
|
||||
edit->installEventFilter(filter);
|
||||
}
|
||||
|
||||
void ShowSystemKeyboard(QWidget* focusWidget)
|
||||
{
|
||||
if (!focusWidget)
|
||||
return;
|
||||
|
||||
focusWidget->setFocus(Qt::MouseFocusReason);
|
||||
|
||||
QInputMethod* inputMethod = QGuiApplication::inputMethod();
|
||||
if (inputMethod)
|
||||
inputMethod->show();
|
||||
|
||||
QTimer::singleShot(0, focusWidget, []() {
|
||||
QInputMethod* delayedInputMethod = QGuiApplication::inputMethod();
|
||||
if (delayedInputMethod)
|
||||
delayedInputMethod->show();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
DialogCameraSetting::DialogCameraSetting(QWidget* parent)
|
||||
: QDialog(parent)
|
||||
@ -13,6 +49,7 @@ DialogCameraSetting::DialogCameraSetting(QWidget* parent)
|
||||
// 输入校验:曝光为微秒,增益为倍数
|
||||
ui->edit_exposure->setValidator(new QDoubleValidator(1.0, 1000000.0, 1, this));
|
||||
ui->edit_gain->setValidator(new QDoubleValidator(0.0, 100.0, 2, this));
|
||||
setupInputKeyboard();
|
||||
}
|
||||
|
||||
DialogCameraSetting::~DialogCameraSetting()
|
||||
@ -51,6 +88,39 @@ void DialogCameraSetting::collectFromUi()
|
||||
if (ui->edit_gain) { bool ok; double v = ui->edit_gain->text().toDouble(&ok); if (ok) m_camera.gain = v; }
|
||||
}
|
||||
|
||||
bool DialogCameraSetting::eventFilter(QObject* watched, QEvent* event)
|
||||
{
|
||||
QLineEdit* edit = qobject_cast<QLineEdit*>(watched);
|
||||
if (edit)
|
||||
{
|
||||
switch (event->type())
|
||||
{
|
||||
case QEvent::FocusIn:
|
||||
case QEvent::MouseButtonPress:
|
||||
case QEvent::MouseButtonRelease:
|
||||
case QEvent::TouchEnd:
|
||||
ShowSystemKeyboard(edit);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return QDialog::eventFilter(watched, event);
|
||||
}
|
||||
|
||||
void DialogCameraSetting::setupInputKeyboard()
|
||||
{
|
||||
const Qt::InputMethodHints ipHints =
|
||||
Qt::ImhPreferNumbers | Qt::ImhNoPredictiveText;
|
||||
const Qt::InputMethodHints numberHints =
|
||||
Qt::ImhPreferNumbers | Qt::ImhFormattedNumbersOnly;
|
||||
|
||||
SetupKeyboardEdit(ui->edit_ip, this, ipHints);
|
||||
SetupKeyboardEdit(ui->edit_exposure, this, numberHints);
|
||||
SetupKeyboardEdit(ui->edit_gain, this, numberHints);
|
||||
}
|
||||
|
||||
void DialogCameraSetting::on_btn_ok_clicked()
|
||||
{
|
||||
collectFromUi();
|
||||
|
||||
@ -37,9 +37,12 @@ private slots:
|
||||
void on_btn_reset_clicked();
|
||||
|
||||
private:
|
||||
bool eventFilter(QObject* watched, QEvent* event) override;
|
||||
void loadToUi(const QString& serverIp, const DroneScrewCameraParams& cam);
|
||||
void collectFromUi(); // 把界面值写回 m_serverIp / m_camera
|
||||
|
||||
void setupInputKeyboard();
|
||||
|
||||
private:
|
||||
Ui::DialogCameraSetting* ui{nullptr};
|
||||
QString m_serverIp;
|
||||
|
||||
166
App/DroneScrewbolt/DroneScrewCtrlApp/dialoghistoryresult.cpp
Normal file
166
App/DroneScrewbolt/DroneScrewCtrlApp/dialoghistoryresult.cpp
Normal file
@ -0,0 +1,166 @@
|
||||
#include "dialoghistoryresult.h"
|
||||
#include "ui_dialoghistoryresult.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QItemSelectionModel>
|
||||
#include <QModelIndex>
|
||||
#include <QStringListModel>
|
||||
#include <QTextStream>
|
||||
#include <algorithm>
|
||||
|
||||
DialogHistoryResult::DialogHistoryResult(const QString& historyDir, QWidget* parent)
|
||||
: QDialog(parent)
|
||||
, ui(new Ui::DialogHistoryResult)
|
||||
, m_historyDir(historyDir)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
setWindowTitle(QStringLiteral("历史汇总结果"));
|
||||
m_listModel = new QStringListModel(this);
|
||||
ui->list_history->setModel(m_listModel);
|
||||
connect(ui->list_history->selectionModel(),
|
||||
&QItemSelectionModel::currentChanged,
|
||||
this,
|
||||
&DialogHistoryResult::onHistoryCurrentChanged);
|
||||
reloadHistory();
|
||||
}
|
||||
|
||||
DialogHistoryResult::~DialogHistoryResult()
|
||||
{
|
||||
delete ui;
|
||||
}
|
||||
|
||||
void DialogHistoryResult::SetHistoryDir(const QString& historyDir)
|
||||
{
|
||||
if (m_historyDir == historyDir)
|
||||
return;
|
||||
|
||||
m_historyDir = historyDir;
|
||||
reloadHistory();
|
||||
}
|
||||
|
||||
void DialogHistoryResult::on_btn_refresh_clicked()
|
||||
{
|
||||
reloadHistory();
|
||||
}
|
||||
|
||||
void DialogHistoryResult::on_btn_close_clicked()
|
||||
{
|
||||
close();
|
||||
}
|
||||
|
||||
void DialogHistoryResult::reloadHistory()
|
||||
{
|
||||
if (!ui->list_history || !ui->edit_detail || !m_listModel)
|
||||
return;
|
||||
|
||||
m_listModel->setStringList(QStringList());
|
||||
ui->edit_detail->clear();
|
||||
m_filePaths.clear();
|
||||
|
||||
QDir dir(m_historyDir);
|
||||
if (!dir.exists())
|
||||
{
|
||||
ui->edit_detail->setPlainText(QStringLiteral("暂无历史数据"));
|
||||
return;
|
||||
}
|
||||
|
||||
QFileInfoList files = dir.entryInfoList(QStringList() << QStringLiteral("*.txt"),
|
||||
QDir::Files | QDir::Readable,
|
||||
QDir::NoSort);
|
||||
std::sort(files.begin(), files.end(), [](const QFileInfo& lhs, const QFileInfo& rhs) {
|
||||
return lhs.lastModified() > rhs.lastModified();
|
||||
});
|
||||
|
||||
QStringList titles;
|
||||
for (const QFileInfo& fileInfo : files)
|
||||
{
|
||||
const QString filePath = fileInfo.absoluteFilePath();
|
||||
const QString content = readHistoryFile(filePath);
|
||||
titles << makeItemTitle(fileInfo, content);
|
||||
m_filePaths << filePath;
|
||||
}
|
||||
m_listModel->setStringList(titles);
|
||||
|
||||
if (m_filePaths.isEmpty())
|
||||
{
|
||||
ui->edit_detail->setPlainText(QStringLiteral("暂无历史数据"));
|
||||
return;
|
||||
}
|
||||
|
||||
ui->list_history->setCurrentIndex(m_listModel->index(0, 0));
|
||||
}
|
||||
|
||||
void DialogHistoryResult::showRecord(int row)
|
||||
{
|
||||
if (!ui->edit_detail)
|
||||
return;
|
||||
|
||||
if (row < 0 || row >= m_filePaths.size())
|
||||
{
|
||||
ui->edit_detail->clear();
|
||||
return;
|
||||
}
|
||||
|
||||
ui->edit_detail->setPlainText(readHistoryFile(m_filePaths.at(row)));
|
||||
}
|
||||
|
||||
void DialogHistoryResult::onHistoryCurrentChanged(const QModelIndex& current,
|
||||
const QModelIndex& previous)
|
||||
{
|
||||
Q_UNUSED(previous);
|
||||
showRecord(current.row());
|
||||
}
|
||||
|
||||
QString DialogHistoryResult::readHistoryFile(const QString& filePath) const
|
||||
{
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||
return QStringLiteral("读取历史数据失败:%1").arg(file.errorString());
|
||||
|
||||
QTextStream in(&file);
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||||
in.setCodec("UTF-8");
|
||||
#endif
|
||||
return in.readAll();
|
||||
}
|
||||
|
||||
QString DialogHistoryResult::makeItemTitle(const QFileInfo& fileInfo, const QString& content) const
|
||||
{
|
||||
QString timeText = fieldValue(content, QStringLiteral("保存时间"));
|
||||
if (timeText.isEmpty())
|
||||
timeText = fileInfo.lastModified().toString(QStringLiteral("yyyy-MM-dd HH:mm:ss"));
|
||||
|
||||
const QString frameId = fieldValue(content, QStringLiteral("frameId"));
|
||||
const QString saveIndex = fieldValue(content, QStringLiteral("saveIndex"));
|
||||
const QString heightCount = fieldValue(content, QStringLiteral("height_mm_count"));
|
||||
const QString spacingCount = fieldValue(content, QStringLiteral("rod_spacing_mm_count"));
|
||||
|
||||
QStringList parts;
|
||||
parts << timeText;
|
||||
if (!saveIndex.isEmpty())
|
||||
parts << QStringLiteral("#%1").arg(saveIndex);
|
||||
if (!frameId.isEmpty())
|
||||
parts << QStringLiteral("帧%1").arg(frameId);
|
||||
if (!heightCount.isEmpty())
|
||||
parts << QStringLiteral("高度%1").arg(heightCount);
|
||||
if (!spacingCount.isEmpty())
|
||||
parts << QStringLiteral("杆距%1").arg(spacingCount);
|
||||
|
||||
return parts.join(QStringLiteral(" "));
|
||||
}
|
||||
|
||||
QString DialogHistoryResult::fieldValue(const QString& content, const QString& fieldName) const
|
||||
{
|
||||
const QString prefix = fieldName + QStringLiteral(":");
|
||||
const QStringList lines = content.split(QLatin1Char('\n'));
|
||||
for (const QString& line : lines)
|
||||
{
|
||||
const QString trimmed = line.trimmed();
|
||||
if (trimmed.startsWith(prefix))
|
||||
return trimmed.mid(prefix.size()).trimmed();
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
43
App/DroneScrewbolt/DroneScrewCtrlApp/dialoghistoryresult.h
Normal file
43
App/DroneScrewbolt/DroneScrewCtrlApp/dialoghistoryresult.h
Normal file
@ -0,0 +1,43 @@
|
||||
#ifndef DRONESCREWCTRL_DIALOG_HISTORY_RESULT_H
|
||||
#define DRONESCREWCTRL_DIALOG_HISTORY_RESULT_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
namespace Ui { class DialogHistoryResult; }
|
||||
|
||||
class QFileInfo;
|
||||
class QModelIndex;
|
||||
class QStringListModel;
|
||||
|
||||
class DialogHistoryResult : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DialogHistoryResult(const QString& historyDir, QWidget* parent = nullptr);
|
||||
~DialogHistoryResult() override;
|
||||
|
||||
void SetHistoryDir(const QString& historyDir);
|
||||
|
||||
private slots:
|
||||
void on_btn_refresh_clicked();
|
||||
void on_btn_close_clicked();
|
||||
|
||||
private:
|
||||
void reloadHistory();
|
||||
void showRecord(int row);
|
||||
void onHistoryCurrentChanged(const QModelIndex& current, const QModelIndex& previous);
|
||||
QString readHistoryFile(const QString& filePath) const;
|
||||
QString makeItemTitle(const QFileInfo& fileInfo, const QString& content) const;
|
||||
QString fieldValue(const QString& content, const QString& fieldName) const;
|
||||
|
||||
private:
|
||||
Ui::DialogHistoryResult* ui{nullptr};
|
||||
QStringListModel* m_listModel{nullptr};
|
||||
QString m_historyDir;
|
||||
QStringList m_filePaths;
|
||||
};
|
||||
|
||||
#endif // DRONESCREWCTRL_DIALOG_HISTORY_RESULT_H
|
||||
82
App/DroneScrewbolt/DroneScrewCtrlApp/dialoghistoryresult.ui
Normal file
82
App/DroneScrewbolt/DroneScrewCtrlApp/dialoghistoryresult.ui
Normal file
@ -0,0 +1,82 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>DialogHistoryResult</class>
|
||||
<widget class="QDialog" name="DialogHistoryResult">
|
||||
<property name="geometry">
|
||||
<rect><x>0</x><y>0</y><width>980</width><height>640</height></rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size><width>760</width><height>480</height></size>
|
||||
</property>
|
||||
<property name="windowTitle"><string>历史汇总结果</string></property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">QDialog { background-color: rgb(25, 26, 28); }
|
||||
QLabel { color: rgb(239, 241, 245); background: transparent; }
|
||||
QListView { background-color: rgb(37, 38, 42); border: 1px solid rgb(51, 53, 60); color: rgb(239, 241, 245); font-size: 15px; outline: none; }
|
||||
QListView::item { min-height: 42px; padding: 8px 10px; border-bottom: 1px solid rgb(51, 53, 60); }
|
||||
QListView::item:selected { background-color: rgb(60, 120, 180); color: rgb(255, 255, 255); }
|
||||
QPlainTextEdit { background-color: rgb(37, 38, 42); border: 1px solid rgb(51, 53, 60); color: rgb(239, 241, 245); font-size: 16px; padding: 10px; }
|
||||
QPushButton { background-color: rgb(47, 48, 52); color: rgb(221, 225, 233); border: 1px solid rgb(75, 78, 86); border-radius: 4px; padding: 8px 18px; font-size: 15px; font-weight: 600; }
|
||||
QPushButton:hover { background-color: rgb(58, 60, 66); }
|
||||
QPushButton:pressed { background-color: rgb(37, 38, 42); }</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="vl_root">
|
||||
<property name="leftMargin"><number>24</number></property>
|
||||
<property name="topMargin"><number>20</number></property>
|
||||
<property name="rightMargin"><number>24</number></property>
|
||||
<property name="bottomMargin"><number>20</number></property>
|
||||
<property name="spacing"><number>16</number></property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_title">
|
||||
<property name="font"><font><pointsize>20</pointsize><bold>true</bold></font></property>
|
||||
<property name="text"><string>历史汇总结果</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="hl_content">
|
||||
<property name="spacing"><number>14</number></property>
|
||||
<item>
|
||||
<widget class="QListView" name="list_history">
|
||||
<property name="minimumSize"><size><width>330</width><height>0</height></size></property>
|
||||
<property name="maximumSize"><size><width>430</width><height>16777215</height></size></property>
|
||||
<property name="editTriggers"><set>QAbstractItemView::NoEditTriggers</set></property>
|
||||
<property name="horizontalScrollBarPolicy"><enum>Qt::ScrollBarAlwaysOff</enum></property>
|
||||
<property name="selectionMode"><enum>QAbstractItemView::SingleSelection</enum></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPlainTextEdit" name="edit_detail">
|
||||
<property name="readOnly"><bool>true</bool></property>
|
||||
<property name="lineWrapMode"><enum>QPlainTextEdit::NoWrap</enum></property>
|
||||
<property name="placeholderText"><string>暂无历史数据</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="hl_buttons">
|
||||
<item>
|
||||
<spacer name="sp_buttons">
|
||||
<property name="orientation"><enum>Qt::Horizontal</enum></property>
|
||||
<property name="sizeHint" stdset="0"><size><width>40</width><height>20</height></size></property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="btn_refresh">
|
||||
<property name="minimumSize"><size><width>100</width><height>44</height></size></property>
|
||||
<property name="text"><string>刷新</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="btn_close">
|
||||
<property name="minimumSize"><size><width>100</width><height>44</height></size></property>
|
||||
<property name="text"><string>关闭</string></property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@ -5,10 +5,12 @@
|
||||
#include "Presenter/Inc/ConfigManager.h"
|
||||
#include "dialogalgoarg.h"
|
||||
#include "dialogcamerasetting.h"
|
||||
#include "dialoghistoryresult.h"
|
||||
#include "resultitem.h"
|
||||
#include "Version.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QFile>
|
||||
#include <QMessageBox>
|
||||
#include <QResizeEvent>
|
||||
#include <QPixmap>
|
||||
@ -31,6 +33,9 @@
|
||||
#include <QPalette>
|
||||
#include <QProgressBar>
|
||||
#include <QPushButton>
|
||||
#include <QStringList>
|
||||
#include <QTextStream>
|
||||
#include <cmath>
|
||||
|
||||
#include "DeviceStatusWidget.h"
|
||||
#include "DetectLogHelper.h"
|
||||
@ -75,6 +80,69 @@ QString windowButtonStyle(const QString& imagePath)
|
||||
.arg(imagePath);
|
||||
}
|
||||
|
||||
bool isDistanceDisplayable(const CtrlDetectionDistance& distance)
|
||||
{
|
||||
return distance.fromId >= 0;
|
||||
}
|
||||
|
||||
int displayableDistanceCount(const std::vector<CtrlDetectionDistance>& distances)
|
||||
{
|
||||
int count = 0;
|
||||
for (const auto& distance : distances)
|
||||
{
|
||||
if (isDistanceDisplayable(distance))
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
bool hasValidHistoryPhysicalHeight(const CtrlDetectionBox& box)
|
||||
{
|
||||
return box.hasPhysicalHeight &&
|
||||
std::isfinite(static_cast<double>(box.physicalHeightMm)) &&
|
||||
box.physicalHeightMm > 0.0f;
|
||||
}
|
||||
|
||||
bool isHistoryDistanceValid(const CtrlDetectionDistance& distance)
|
||||
{
|
||||
return distance.fromId >= 0 &&
|
||||
std::isfinite(static_cast<double>(distance.distanceMm));
|
||||
}
|
||||
|
||||
QString boolText(bool value)
|
||||
{
|
||||
return value ? QStringLiteral("true") : QStringLiteral("false");
|
||||
}
|
||||
|
||||
QString finalFailureText(const CtrlDetectionFrame& frame)
|
||||
{
|
||||
return frame.message.isEmpty() ? QStringLiteral("检测失败") : frame.message;
|
||||
}
|
||||
|
||||
QString physicalHeightHistoryErrorText(const CtrlDetectionFrame& frame,
|
||||
const CtrlDetectionBox& box)
|
||||
{
|
||||
if (!frame.success)
|
||||
return finalFailureText(frame);
|
||||
|
||||
QStringList parts;
|
||||
parts << QStringLiteral("未测到物理高度");
|
||||
|
||||
const QString status = box.matchStatus.trimmed();
|
||||
if (!status.isEmpty() && status != QStringLiteral("ok"))
|
||||
parts << QStringLiteral("状态:%1").arg(status);
|
||||
|
||||
if (!box.hasStereoMatch)
|
||||
parts << QStringLiteral("双目未匹配");
|
||||
else if (!box.trusted)
|
||||
parts << QStringLiteral("结果不可信");
|
||||
|
||||
if (box.hasPhysicalHeight)
|
||||
parts << QStringLiteral("高度无效:%1 mm").arg(box.physicalHeightMm, 0, 'f', 2);
|
||||
|
||||
return parts.join(QStringLiteral(" "));
|
||||
}
|
||||
|
||||
void applyPullingDialogTheme(QProgressDialog& dialog)
|
||||
{
|
||||
const QColor windowColor(kThemeWindowR, kThemeWindowG, kThemeWindowB);
|
||||
@ -244,7 +312,10 @@ void MainWindow::initUi()
|
||||
if (ui->btn_algo_config)
|
||||
ui->btn_algo_config->setStyleSheet(iconButtonStyle(QStringLiteral(":/common/resource/config_algo.png")));
|
||||
if (ui->btn_test)
|
||||
{
|
||||
ui->btn_test->setStyleSheet(iconButtonStyle(QStringLiteral(":/common/resource/config_data_test.png")));
|
||||
ui->btn_test->setToolTip(QStringLiteral("历史数据"));
|
||||
}
|
||||
if (ui->btn_close)
|
||||
ui->btn_close->setStyleSheet(windowButtonStyle(QStringLiteral(":/common/resource/close.png")));
|
||||
if (ui->btn_hide)
|
||||
@ -292,6 +363,9 @@ void MainWindow::OnRtspFrame(const QImage& frame)
|
||||
|
||||
void MainWindow::OnDetectionResult(const CtrlDetectionFrame& result)
|
||||
{
|
||||
if (result.isFinalResult)
|
||||
saveFinalResultHistory(result);
|
||||
|
||||
if (!ui->detect_result_list) return;
|
||||
|
||||
auto upsertResultItem = [this, &result](int row,
|
||||
@ -342,17 +416,22 @@ void MainWindow::OnDetectionResult(const CtrlDetectionFrame& result)
|
||||
int row = 0;
|
||||
if (m_resultDisplayMode == ResultDisplayMode::Distance)
|
||||
{
|
||||
const int distanceCount = displayableDistanceCount(result.distances);
|
||||
const QString head = QStringLiteral("帧#%1 测距 %2")
|
||||
.arg(result.frameId)
|
||||
.arg(static_cast<int>(result.distances.size()));
|
||||
if (result.distances.empty())
|
||||
.arg(distanceCount);
|
||||
if (distanceCount <= 0)
|
||||
{
|
||||
upsertResultItem(row++, head, -1, ResultItem::DisplayMode::Distance);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (size_t i = 0; i < result.distances.size(); ++i)
|
||||
{
|
||||
if (!isDistanceDisplayable(result.distances[i]))
|
||||
continue;
|
||||
upsertResultItem(row++, head, static_cast<int>(i), ResultItem::DisplayMode::Distance);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (result.boxes.empty() && result.distances.empty())
|
||||
@ -420,6 +499,22 @@ void MainWindow::onServerInfoReceived(const WDRemoteServerInfo& info)
|
||||
.arg(static_cast<double>(info.algoScore), 0, 'f', 2));
|
||||
}
|
||||
|
||||
bool MainWindow::refreshServerInfoFromServer()
|
||||
{
|
||||
if (!m_pPresenter)
|
||||
return false;
|
||||
|
||||
WDRemoteServerInfo info;
|
||||
if (!m_pPresenter->QueryServerInfo(info))
|
||||
{
|
||||
appendLog(QStringLiteral("读取服务器参数失败,使用当前缓存/本地配置"));
|
||||
return false;
|
||||
}
|
||||
|
||||
onServerInfoReceived(info);
|
||||
return true;
|
||||
}
|
||||
|
||||
void MainWindow::OnStatusMessage(const QString& msg)
|
||||
{
|
||||
appendLog(msg);
|
||||
@ -448,6 +543,155 @@ void MainWindow::clearResultList()
|
||||
ui->detect_result_list->clear();
|
||||
}
|
||||
|
||||
QString MainWindow::historyRootDir() const
|
||||
{
|
||||
QString homePath = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
|
||||
if (homePath.isEmpty())
|
||||
homePath = QDir::homePath();
|
||||
|
||||
return QDir(homePath).filePath(QStringLiteral("DroneScrewCtrlApp/history"));
|
||||
}
|
||||
|
||||
QString MainWindow::finalResultHistoryKey(const CtrlDetectionFrame& result) const
|
||||
{
|
||||
return QStringLiteral("%1:%2:%3")
|
||||
.arg(result.saveIndex)
|
||||
.arg(result.frameId)
|
||||
.arg(result.timestampUs);
|
||||
}
|
||||
|
||||
QString MainWindow::makeFinalResultHistoryText(const CtrlDetectionFrame& result,
|
||||
const QDateTime& savedAt) const
|
||||
{
|
||||
QString text;
|
||||
QTextStream out(&text);
|
||||
|
||||
int validHeightCount = 0;
|
||||
for (const CtrlDetectionBox& box : result.boxes)
|
||||
{
|
||||
if (hasValidHistoryPhysicalHeight(box))
|
||||
++validHeightCount;
|
||||
}
|
||||
|
||||
int validDistanceCount = 0;
|
||||
for (const CtrlDetectionDistance& distance : result.distances)
|
||||
{
|
||||
if (isHistoryDistanceValid(distance))
|
||||
++validDistanceCount;
|
||||
}
|
||||
|
||||
out << QStringLiteral("保存时间:") << savedAt.toString(QStringLiteral("yyyy-MM-dd HH:mm:ss.zzz")) << "\n";
|
||||
out << QStringLiteral("结果类型:final") << "\n";
|
||||
out << QStringLiteral("frameId:") << result.frameId << "\n";
|
||||
out << QStringLiteral("saveIndex:") << result.saveIndex << "\n";
|
||||
out << QStringLiteral("timestampUs:") << result.timestampUs << "\n";
|
||||
out << QStringLiteral("success:") << boolText(result.success) << "\n";
|
||||
out << QStringLiteral("errorCode:") << result.errorCode << "\n";
|
||||
if (!result.message.isEmpty())
|
||||
out << QStringLiteral("message:") << result.message << "\n";
|
||||
out << QStringLiteral("rankScore:") << QString::number(result.rankScore, 'f', 6) << "\n";
|
||||
out << QStringLiteral("rankSampleCount:") << result.rankSampleCount << "\n";
|
||||
out << QStringLiteral("target_count:") << static_cast<int>(result.boxes.size()) << "\n";
|
||||
out << QStringLiteral("height_mm_count:") << validHeightCount << "\n";
|
||||
|
||||
if (result.boxes.empty())
|
||||
out << QStringLiteral("height_mm_error:未测到物理高度") << "\n";
|
||||
|
||||
for (size_t i = 0; i < result.boxes.size(); ++i)
|
||||
{
|
||||
const CtrlDetectionBox& box = result.boxes[i];
|
||||
const int targetIndex = static_cast<int>(i + 1);
|
||||
const QString prefix = QStringLiteral("height_mm_%1").arg(targetIndex);
|
||||
const QString status = box.matchStatus.trimmed().isEmpty()
|
||||
? QStringLiteral("-")
|
||||
: box.matchStatus.trimmed();
|
||||
|
||||
if (hasValidHistoryPhysicalHeight(box))
|
||||
out << prefix << QStringLiteral(":") << QString::number(box.physicalHeightMm, 'f', 2) << "\n";
|
||||
else
|
||||
{
|
||||
out << prefix << QStringLiteral(":ERROR") << "\n";
|
||||
out << prefix << QStringLiteral("_error:") << physicalHeightHistoryErrorText(result, box) << "\n";
|
||||
}
|
||||
out << prefix << QStringLiteral("_status:") << status << "\n";
|
||||
out << prefix << QStringLiteral("_trusted:") << boolText(box.trusted) << "\n";
|
||||
out << prefix << QStringLiteral("_stereo_match:") << boolText(box.hasStereoMatch) << "\n";
|
||||
out << prefix << QStringLiteral("_score:") << QString::number(box.score, 'f', 6) << "\n";
|
||||
}
|
||||
|
||||
out << QStringLiteral("rod_spacing_mm_count:") << validDistanceCount << "\n";
|
||||
if (validDistanceCount <= 0)
|
||||
out << QStringLiteral("rod_spacing_mm_error:未测到杆间距离") << "\n";
|
||||
|
||||
int distanceIndex = 1;
|
||||
for (const CtrlDetectionDistance& distance : result.distances)
|
||||
{
|
||||
if (!isHistoryDistanceValid(distance))
|
||||
continue;
|
||||
|
||||
const QString prefix = QStringLiteral("rod_spacing_mm_%1").arg(distanceIndex);
|
||||
const QString fromText = QString::number(distance.fromId + 1);
|
||||
const QString toText = distance.toId >= 0
|
||||
? QString::number(distance.toId + 1)
|
||||
: QStringLiteral("-");
|
||||
|
||||
out << prefix << QStringLiteral(":") << QString::number(distance.distanceMm, 'f', 2) << "\n";
|
||||
out << prefix << QStringLiteral("_from:") << fromText << "\n";
|
||||
out << prefix << QStringLiteral("_to:") << toText << "\n";
|
||||
out << prefix << QStringLiteral("_pair:") << fromText << QStringLiteral("-") << toText << "\n";
|
||||
++distanceIndex;
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
bool MainWindow::saveFinalResultHistory(const CtrlDetectionFrame& result)
|
||||
{
|
||||
if (!result.isFinalResult)
|
||||
return false;
|
||||
|
||||
const bool hasStableKey = (result.saveIndex > 0 || result.frameId > 0 || result.timestampUs > 0);
|
||||
const QString historyKey = finalResultHistoryKey(result);
|
||||
if (hasStableKey && historyKey == m_lastStoredFinalResultKey)
|
||||
return true;
|
||||
|
||||
const QString dirPath = historyRootDir();
|
||||
if (!QDir().mkpath(dirPath))
|
||||
{
|
||||
appendLog(QStringLiteral("创建历史结果目录失败:%1").arg(dirPath));
|
||||
return false;
|
||||
}
|
||||
|
||||
const QDateTime savedAt = QDateTime::currentDateTime();
|
||||
QString fileName = savedAt.toString(QStringLiteral("yyyyMMdd_HHmmss_zzz"));
|
||||
if (result.saveIndex > 0)
|
||||
fileName += QStringLiteral("_save_%1").arg(result.saveIndex);
|
||||
if (result.frameId > 0)
|
||||
fileName += QStringLiteral("_frame_%1").arg(result.frameId);
|
||||
fileName += QStringLiteral(".txt");
|
||||
|
||||
QFile file(QDir(dirPath).filePath(fileName));
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate))
|
||||
{
|
||||
appendLog(QStringLiteral("保存历史结果失败:%1").arg(file.errorString()));
|
||||
return false;
|
||||
}
|
||||
|
||||
QTextStream out(&file);
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||||
out.setCodec("UTF-8");
|
||||
#endif
|
||||
out << makeFinalResultHistoryText(result, savedAt);
|
||||
out.flush();
|
||||
file.close();
|
||||
|
||||
if (hasStableKey)
|
||||
m_lastStoredFinalResultKey = historyKey;
|
||||
|
||||
appendLog(QStringLiteral("已保存最终汇总结果:%1").arg(QDir(dirPath).filePath(fileName)));
|
||||
return true;
|
||||
}
|
||||
|
||||
void MainWindow::resizeEvent(QResizeEvent* event)
|
||||
{
|
||||
QMainWindow::resizeEvent(event);
|
||||
@ -563,8 +807,34 @@ void MainWindow::on_btn_start_clicked()
|
||||
appendLog(QStringLiteral("正在请求 Server 停止..."));
|
||||
ui->btn_start->setEnabled(false);
|
||||
ui->btn_detect_start->setEnabled(false);
|
||||
|
||||
QProgressDialog* finalizingDialog = nullptr;
|
||||
if (m_uiState == UiState::Detect)
|
||||
{
|
||||
finalizingDialog = new QProgressDialog(QStringLiteral("正在汇总计算..."),
|
||||
QString(),
|
||||
0,
|
||||
0,
|
||||
this);
|
||||
finalizingDialog->setWindowTitle(QStringLiteral("提示"));
|
||||
finalizingDialog->setCancelButton(nullptr);
|
||||
finalizingDialog->setWindowModality(Qt::ApplicationModal);
|
||||
finalizingDialog->setMinimumDuration(0);
|
||||
finalizingDialog->setAutoClose(false);
|
||||
finalizingDialog->setAutoReset(false);
|
||||
applyPullingDialogTheme(*finalizingDialog);
|
||||
finalizingDialog->show();
|
||||
applyPullingDialogTheme(*finalizingDialog);
|
||||
}
|
||||
QApplication::processEvents();
|
||||
const bool ok = m_pPresenter && m_pPresenter->StopAll();
|
||||
if (finalizingDialog)
|
||||
{
|
||||
finalizingDialog->close();
|
||||
delete finalizingDialog;
|
||||
finalizingDialog = nullptr;
|
||||
}
|
||||
QApplication::processEvents();
|
||||
if (ok)
|
||||
{
|
||||
appendLog(QStringLiteral("已请求 Server 停止"));
|
||||
@ -621,6 +891,7 @@ void MainWindow::on_btn_camera_clicked()
|
||||
{
|
||||
// 「相机」按钮 → 相机设置:Server IP + 曝光时间(μs) + 增益
|
||||
auto cfg0 = ConfigManager::Instance().GetConfig();
|
||||
refreshServerInfoFromServer();
|
||||
|
||||
DialogCameraSetting dlg(this);
|
||||
|
||||
@ -661,6 +932,7 @@ void MainWindow::on_btn_camera_clicked()
|
||||
void MainWindow::on_btn_algo_config_clicked()
|
||||
{
|
||||
auto cfg = ConfigManager::Instance().GetConfig();
|
||||
refreshServerInfoFromServer();
|
||||
DialogAlgoArg dlg(this);
|
||||
|
||||
// 优先使用从 Server 读取的参数,没有则用本地配置
|
||||
@ -671,8 +943,8 @@ void MainWindow::on_btn_algo_config_clicked()
|
||||
serverParams.nmsThreshold = m_cachedServerInfo.algoNms;
|
||||
serverParams.inputWidth = m_cachedServerInfo.algoWidth;
|
||||
serverParams.inputHeight = m_cachedServerInfo.algoHeight;
|
||||
serverParams.modelPath = m_cachedServerInfo.algoModel;
|
||||
serverParams.modelType = m_cachedServerInfo.algoModelType;
|
||||
serverParams.expectedBoltCount = m_cachedServerInfo.algoExpectedBoltCount;
|
||||
dlg.SetParams(serverParams);
|
||||
}
|
||||
else
|
||||
@ -696,11 +968,8 @@ void MainWindow::on_btn_algo_config_clicked()
|
||||
|
||||
void MainWindow::on_btn_test_clicked()
|
||||
{
|
||||
m_resultDisplayMode = ResultDisplayMode::Precision;
|
||||
clearResultList();
|
||||
// 「测试」按钮 → 单次检测请求
|
||||
if (m_pPresenter && m_pPresenter->TriggerSingleDetection())
|
||||
appendLog(QStringLiteral("已发送单次检测请求"));
|
||||
DialogHistoryResult dlg(historyRootDir(), this);
|
||||
dlg.exec();
|
||||
}
|
||||
|
||||
// ======================== 页面布局 ========================
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
#include <QLabel>
|
||||
#include <QGraphicsScene>
|
||||
#include <QGraphicsPixmapItem>
|
||||
#include <QDateTime>
|
||||
#include <QSize>
|
||||
#include <QStringListModel>
|
||||
#include <QVBoxLayout>
|
||||
@ -44,7 +45,7 @@ private slots:
|
||||
void on_btn_hide_clicked();
|
||||
void on_btn_camera_clicked(); // 连接配置
|
||||
void on_btn_algo_config_clicked(); // 算法参数
|
||||
void on_btn_test_clicked(); // 单次检测
|
||||
void on_btn_test_clicked(); // 历史数据
|
||||
void onServerInfoReceived(const WDRemoteServerInfo& info); // 服务器参数接收
|
||||
|
||||
protected:
|
||||
@ -57,6 +58,12 @@ private:
|
||||
void appendLog(const QString& msg);
|
||||
void clampResultList();
|
||||
void clearResultList();
|
||||
bool refreshServerInfoFromServer();
|
||||
QString historyRootDir() const;
|
||||
QString finalResultHistoryKey(const CtrlDetectionFrame& result) const;
|
||||
QString makeFinalResultHistoryText(const CtrlDetectionFrame& result,
|
||||
const QDateTime& savedAt) const;
|
||||
bool saveFinalResultHistory(const CtrlDetectionFrame& result);
|
||||
|
||||
// 顶部按钮状态机
|
||||
enum class UiState { Idle, Live, Detect };
|
||||
@ -89,6 +96,7 @@ private:
|
||||
|
||||
int m_maxResultItems{200};
|
||||
WDRemoteServerInfo m_cachedServerInfo; // 从 Server 读取的参数缓存
|
||||
QString m_lastStoredFinalResultKey;
|
||||
};
|
||||
|
||||
#endif // DRONESCREWCTRL_MAINWINDOW_H
|
||||
|
||||
@ -239,7 +239,7 @@
|
||||
</font>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>单次检测</string>
|
||||
<string>历史数据</string>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">image: url(:/common/resource/config_data_test.png); background-color: rgb(38, 40, 47); border: none;</string>
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
|
||||
#include <QStringList>
|
||||
#include <QFrame>
|
||||
#include <cmath>
|
||||
|
||||
namespace
|
||||
{
|
||||
@ -31,6 +32,46 @@ QString failedText(const CtrlDetectionFrame& frame)
|
||||
return frame.message.isEmpty() ? QStringLiteral("检测失败") : frame.message;
|
||||
}
|
||||
|
||||
bool hasValidPhysicalHeight(const CtrlDetectionBox& box)
|
||||
{
|
||||
return box.hasPhysicalHeight &&
|
||||
std::isfinite(static_cast<double>(box.physicalHeightMm)) &&
|
||||
box.physicalHeightMm > 0.0f;
|
||||
}
|
||||
|
||||
QString physicalHeightErrorText(const CtrlDetectionFrame& frame,
|
||||
const CtrlDetectionBox& box)
|
||||
{
|
||||
if (!frame.success)
|
||||
return failedText(frame);
|
||||
|
||||
QStringList parts;
|
||||
parts << QStringLiteral("未测到物理高度");
|
||||
|
||||
const QString status = box.matchStatus.trimmed();
|
||||
if (!status.isEmpty() && status != QStringLiteral("ok"))
|
||||
parts << QStringLiteral("状态:%1").arg(status);
|
||||
|
||||
if (!box.hasStereoMatch)
|
||||
parts << QStringLiteral("双目未匹配");
|
||||
else if (!box.trusted)
|
||||
parts << QStringLiteral("结果不可信");
|
||||
|
||||
if (box.hasPhysicalHeight)
|
||||
parts << QStringLiteral("高度无效:%1 mm").arg(box.physicalHeightMm, 0, 'f', 2);
|
||||
|
||||
return parts.join(QStringLiteral(" "));
|
||||
}
|
||||
|
||||
void setStatusErrorStyle(Ui::ResultItem* ui)
|
||||
{
|
||||
if (!ui || !ui->label_status)
|
||||
return;
|
||||
|
||||
ui->label_status->setStyleSheet(QStringLiteral(
|
||||
"color: rgb(255, 105, 105); background-color: rgb(37, 38, 42); font-size: 22px; font-weight: 700;"));
|
||||
}
|
||||
|
||||
QString oneDistanceText(const CtrlDetectionDistance& d)
|
||||
{
|
||||
const double meters = d.distanceMm / 1000.0;
|
||||
@ -42,14 +83,27 @@ QString oneDistanceMmText(const CtrlDetectionDistance& d)
|
||||
return QStringLiteral("%1 mm").arg(d.distanceMm, 0, 'f', 2);
|
||||
}
|
||||
|
||||
bool isDistanceDisplayable(const CtrlDetectionDistance& distance)
|
||||
{
|
||||
return distance.fromId >= 0;
|
||||
}
|
||||
|
||||
QString distanceText(const CtrlDetectionFrame& frame, int targetIndex)
|
||||
{
|
||||
if (targetIndex >= 0 && targetIndex < static_cast<int>(frame.distances.size()))
|
||||
return oneDistanceText(frame.distances[static_cast<size_t>(targetIndex)]);
|
||||
{
|
||||
const CtrlDetectionDistance& distance = frame.distances[static_cast<size_t>(targetIndex)];
|
||||
return isDistanceDisplayable(distance)
|
||||
? oneDistanceText(distance)
|
||||
: QStringLiteral("未测到距离");
|
||||
}
|
||||
|
||||
QStringList distanceParts;
|
||||
for (size_t i = 0; i < frame.distances.size(); ++i)
|
||||
distanceParts << oneDistanceText(frame.distances[i]);
|
||||
{
|
||||
if (isDistanceDisplayable(frame.distances[i]))
|
||||
distanceParts << oneDistanceText(frame.distances[i]);
|
||||
}
|
||||
return distanceParts.isEmpty()
|
||||
? QStringLiteral("未测到距离")
|
||||
: distanceParts.join(" ");
|
||||
@ -58,11 +112,19 @@ QString distanceText(const CtrlDetectionFrame& frame, int targetIndex)
|
||||
QString distanceMmText(const CtrlDetectionFrame& frame, int targetIndex)
|
||||
{
|
||||
if (targetIndex >= 0 && targetIndex < static_cast<int>(frame.distances.size()))
|
||||
return oneDistanceMmText(frame.distances[static_cast<size_t>(targetIndex)]);
|
||||
{
|
||||
const CtrlDetectionDistance& distance = frame.distances[static_cast<size_t>(targetIndex)];
|
||||
return isDistanceDisplayable(distance)
|
||||
? oneDistanceMmText(distance)
|
||||
: QStringLiteral("未测到距离");
|
||||
}
|
||||
|
||||
QStringList distanceParts;
|
||||
for (size_t i = 0; i < frame.distances.size(); ++i)
|
||||
distanceParts << oneDistanceMmText(frame.distances[i]);
|
||||
{
|
||||
if (isDistanceDisplayable(frame.distances[i]))
|
||||
distanceParts << oneDistanceMmText(frame.distances[i]);
|
||||
}
|
||||
return distanceParts.isEmpty()
|
||||
? QStringLiteral("未测到距离")
|
||||
: distanceParts.join(" ");
|
||||
@ -74,7 +136,8 @@ QString distanceIndexText(const CtrlDetectionFrame& frame, int targetIndex)
|
||||
if (targetIndex >= 0 && targetIndex < total)
|
||||
{
|
||||
const CtrlDetectionDistance& d = frame.distances[static_cast<size_t>(targetIndex)];
|
||||
return QString::number(d.toId + 1);
|
||||
if (isDistanceDisplayable(d))
|
||||
return QString::number(d.fromId + 1);
|
||||
}
|
||||
return QStringLiteral("-");
|
||||
}
|
||||
@ -145,11 +208,16 @@ void ResultItem::setResultData(int targetIndex,
|
||||
{
|
||||
const bool precisionMode = (mode == DisplayMode::Precision);
|
||||
const bool precisionDistanceMode = (mode == DisplayMode::PrecisionDistance);
|
||||
const QString finalPrefix =
|
||||
(frame.isFinalResult && frame.saveIndex > 0)
|
||||
? QStringLiteral("#%1 ").arg(frame.saveIndex)
|
||||
: QString();
|
||||
setResultTextStyle(ui);
|
||||
setMinimumHeight((precisionMode || precisionDistanceMode) ? 104 : 108);
|
||||
|
||||
if (ui->lb_id_t)
|
||||
ui->lb_id_t->setText(QStringLiteral("ID"));
|
||||
ui->lb_id_t->setText(frame.isFinalResult ? QStringLiteral("FINAL")
|
||||
: QStringLiteral("ID"));
|
||||
if (ui->lb_status_t)
|
||||
{
|
||||
if (mode == DisplayMode::Distance)
|
||||
@ -165,19 +233,19 @@ void ResultItem::setResultData(int targetIndex,
|
||||
if (ui->label_id)
|
||||
{
|
||||
if (precisionMode && targetIndex >= 0)
|
||||
ui->label_id->setText(QString("%1/%2")
|
||||
.arg(targetIndex + 1)
|
||||
.arg(static_cast<int>(frame.boxes.size())));
|
||||
ui->label_id->setText(finalPrefix + QString("%1/%2")
|
||||
.arg(targetIndex + 1)
|
||||
.arg(static_cast<int>(frame.boxes.size())));
|
||||
else if (precisionDistanceMode)
|
||||
{
|
||||
ui->label_id->setText(precisionDistanceIndexText(frame, targetIndex));
|
||||
ui->label_id->setText(finalPrefix + precisionDistanceIndexText(frame, targetIndex));
|
||||
}
|
||||
else if (mode == DisplayMode::Distance)
|
||||
{
|
||||
ui->label_id->setText(distanceIndexText(frame, targetIndex));
|
||||
ui->label_id->setText(finalPrefix + distanceIndexText(frame, targetIndex));
|
||||
}
|
||||
else
|
||||
ui->label_id->setText(QStringLiteral("0/0"));
|
||||
ui->label_id->setText(finalPrefix + QStringLiteral("0/0"));
|
||||
}
|
||||
|
||||
if (mode == DisplayMode::Distance || precisionDistanceMode)
|
||||
@ -198,11 +266,16 @@ void ResultItem::setResultData(int targetIndex,
|
||||
if (targetIndex >= 0 && targetIndex < static_cast<int>(frame.boxes.size()))
|
||||
{
|
||||
const CtrlDetectionBox& b = frame.boxes[static_cast<size_t>(targetIndex)];
|
||||
if (b.hasPhysicalHeight)
|
||||
if (hasValidPhysicalHeight(b))
|
||||
{
|
||||
ui->label_status->setText(QStringLiteral("%1 mm")
|
||||
.arg(b.physicalHeightMm, 0, 'f', 2));
|
||||
}
|
||||
else if (precisionMode)
|
||||
{
|
||||
ui->label_status->setText(physicalHeightErrorText(frame, b));
|
||||
setStatusErrorStyle(ui);
|
||||
}
|
||||
else
|
||||
{
|
||||
const QString text = QStringLiteral("score=%1 w=%2 h=%3")
|
||||
|
||||
@ -31,7 +31,7 @@ struct DroneScrewAlgoUiParams
|
||||
int inputWidth{640};
|
||||
int inputHeight{640};
|
||||
int modelType{0};
|
||||
std::string modelPath;
|
||||
int expectedBoltCount{8};
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -76,7 +76,7 @@ int CVrDroneScrewCtrlConfig::LoadConfig(const std::string& filePath,
|
||||
result.algo.inputWidth = IntAttr(a, "inputWidth", result.algo.inputWidth);
|
||||
result.algo.inputHeight = IntAttr(a, "inputHeight", result.algo.inputHeight);
|
||||
result.algo.modelType = IntAttr(a, "modelType", result.algo.modelType);
|
||||
result.algo.modelPath = SafeAttr(a, "modelPath", result.algo.modelPath);
|
||||
result.algo.expectedBoltCount = IntAttr(a, "expectedBoltCount", result.algo.expectedBoltCount);
|
||||
}
|
||||
if (XMLElement* c = root->FirstChildElement("Camera"))
|
||||
{
|
||||
@ -121,7 +121,7 @@ bool CVrDroneScrewCtrlConfig::SaveConfig(const std::string& filePath,
|
||||
a->SetAttribute("inputWidth", result.algo.inputWidth);
|
||||
a->SetAttribute("inputHeight", result.algo.inputHeight);
|
||||
a->SetAttribute("modelType", result.algo.modelType);
|
||||
a->SetAttribute("modelPath", result.algo.modelPath.c_str());
|
||||
a->SetAttribute("expectedBoltCount", result.algo.expectedBoltCount);
|
||||
root->InsertEndChild(a);
|
||||
|
||||
XMLElement* c = doc.NewElement("Camera");
|
||||
|
||||
@ -707,7 +707,15 @@ void DroneScrewAlgoStub::appendBoltBox(const StereoBoltModuleBoltC& bolt,
|
||||
if (mapRectifiedLeftRoi(bolt.left_roi, result.imageWidth, result.imageHeight, box))
|
||||
{
|
||||
box.hasPhysicalHeight = true;
|
||||
// height_mm is the physical bolt height used by CtrlApp display and txt storage.
|
||||
box.physicalHeightMm = static_cast<float>(bolt.height_mm);
|
||||
box.hasStereoMatch = bolt.idx_L >= 0 && bolt.idx_R >= 0 && bolt.pair_id >= 0;
|
||||
box.trusted = bolt.confidence == 0;
|
||||
box.matchConfidence = bolt.confidence;
|
||||
box.matchStatus = bolt.status;
|
||||
box.pairId = bolt.pair_id;
|
||||
box.leftIndex = bolt.idx_L;
|
||||
box.rightIndex = bolt.idx_R;
|
||||
result.boxes.push_back(box);
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,6 +18,13 @@ struct DroneScrewBox
|
||||
int height{0};
|
||||
bool hasPhysicalHeight{false};
|
||||
float physicalHeightMm{0.0f};
|
||||
bool hasStereoMatch{false};
|
||||
bool trusted{false};
|
||||
int matchConfidence{1};
|
||||
std::string matchStatus;
|
||||
int pairId{-1};
|
||||
int leftIndex{-1};
|
||||
int rightIndex{-1};
|
||||
};
|
||||
|
||||
/**
|
||||
@ -43,6 +50,10 @@ struct DroneScrewResult
|
||||
bool success{true};
|
||||
int errorCode{0};
|
||||
std::string message;
|
||||
bool isFinalResult{false};
|
||||
double rankScore{0.0};
|
||||
int rankSampleCount{0};
|
||||
uint64_t saveIndex{0};
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -16,8 +16,10 @@
|
||||
#include <QXmlStreamReader>
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <new>
|
||||
#include <sstream>
|
||||
@ -69,6 +71,7 @@ constexpr int kDetectPipelineDistance = 1;
|
||||
constexpr const char* kImageSaveRootDir = "/home/cat";
|
||||
constexpr size_t kMaxImageSaveQueueDepth = 100;
|
||||
constexpr size_t kImageSaveWorkerCount = 4;
|
||||
constexpr int kDefaultPrecisionExpectedCount = 8;
|
||||
|
||||
const char* mvsSdkErrorName(int code)
|
||||
{
|
||||
@ -252,6 +255,256 @@ void logDetectionResultFixed(const char* tag, int frameNo, const DroneScrewResul
|
||||
}
|
||||
}
|
||||
|
||||
bool positiveFinite(double value)
|
||||
{
|
||||
return std::isfinite(value) && value > 0.0;
|
||||
}
|
||||
|
||||
int expectedBoltCountOrDefault(int expectedCount)
|
||||
{
|
||||
return expectedCount > 0 ? expectedCount : kDefaultPrecisionExpectedCount;
|
||||
}
|
||||
|
||||
int expectedDistanceCount(int expectedCount)
|
||||
{
|
||||
return std::max(0, expectedCount - 1);
|
||||
}
|
||||
|
||||
int physicalHeightCount(const DroneScrewResult& result)
|
||||
{
|
||||
int count = 0;
|
||||
for (const auto& b : result.boxes)
|
||||
{
|
||||
if (b.hasPhysicalHeight && positiveFinite(b.physicalHeightMm))
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
bool boltStatusOk(const DroneScrewBox& box)
|
||||
{
|
||||
return box.matchStatus == "ok";
|
||||
}
|
||||
|
||||
int stereoMatchedPhysicalHeightCount(const DroneScrewResult& result)
|
||||
{
|
||||
int count = 0;
|
||||
for (const auto& b : result.boxes)
|
||||
{
|
||||
if (b.hasPhysicalHeight &&
|
||||
positiveFinite(b.physicalHeightMm) &&
|
||||
b.hasStereoMatch)
|
||||
{
|
||||
++count;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
int trustedStatusOkPhysicalHeightCount(const DroneScrewResult& result)
|
||||
{
|
||||
int count = 0;
|
||||
for (const auto& b : result.boxes)
|
||||
{
|
||||
if (b.hasPhysicalHeight &&
|
||||
positiveFinite(b.physicalHeightMm) &&
|
||||
b.hasStereoMatch &&
|
||||
b.trusted &&
|
||||
boltStatusOk(b))
|
||||
{
|
||||
++count;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
int scoreOkPhysicalHeightCount(const DroneScrewResult& result, double scoreThreshold)
|
||||
{
|
||||
int count = 0;
|
||||
for (const auto& b : result.boxes)
|
||||
{
|
||||
if (b.hasPhysicalHeight &&
|
||||
positiveFinite(b.physicalHeightMm) &&
|
||||
static_cast<double>(b.score) >= scoreThreshold)
|
||||
{
|
||||
++count;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
int validDistanceCount(const DroneScrewResult& result)
|
||||
{
|
||||
int count = 0;
|
||||
for (const auto& d : result.distances)
|
||||
{
|
||||
if (positiveFinite(d.distanceMm))
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
double averageDetectionScore(const DroneScrewResult& result)
|
||||
{
|
||||
double sum = 0.0;
|
||||
int count = 0;
|
||||
for (const auto& b : result.boxes)
|
||||
{
|
||||
if (!b.hasPhysicalHeight ||
|
||||
!positiveFinite(b.physicalHeightMm) ||
|
||||
!std::isfinite(static_cast<double>(b.score)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
sum += static_cast<double>(b.score);
|
||||
++count;
|
||||
}
|
||||
return count > 0 ? (sum / static_cast<double>(count)) : 0.0;
|
||||
}
|
||||
|
||||
double medianValue(std::vector<double> values)
|
||||
{
|
||||
if (values.empty())
|
||||
return 0.0;
|
||||
std::sort(values.begin(), values.end());
|
||||
const size_t mid = values.size() / 2;
|
||||
if ((values.size() % 2) != 0)
|
||||
return values[mid];
|
||||
return (values[mid - 1] + values[mid]) * 0.5;
|
||||
}
|
||||
|
||||
std::vector<double> sessionMedianPhysicalHeights(const std::vector<DroneScrewResult>& results,
|
||||
int expectedCount)
|
||||
{
|
||||
std::vector<double> medians(static_cast<size_t>(expectedCount), 0.0);
|
||||
for (int i = 0; i < expectedCount; ++i)
|
||||
{
|
||||
std::vector<double> values;
|
||||
for (const auto& result : results)
|
||||
{
|
||||
if (!result.success || result.errorCode != 0)
|
||||
continue;
|
||||
if (i >= static_cast<int>(result.boxes.size()))
|
||||
continue;
|
||||
const auto& b = result.boxes[static_cast<size_t>(i)];
|
||||
if (b.hasPhysicalHeight && positiveFinite(b.physicalHeightMm))
|
||||
values.push_back(static_cast<double>(b.physicalHeightMm));
|
||||
}
|
||||
medians[static_cast<size_t>(i)] = medianValue(std::move(values));
|
||||
}
|
||||
return medians;
|
||||
}
|
||||
|
||||
std::vector<double> sessionMedianDistances(const std::vector<DroneScrewResult>& results,
|
||||
int distanceCount)
|
||||
{
|
||||
std::vector<double> medians(static_cast<size_t>(distanceCount), 0.0);
|
||||
for (int i = 0; i < distanceCount; ++i)
|
||||
{
|
||||
std::vector<double> values;
|
||||
for (const auto& result : results)
|
||||
{
|
||||
if (!result.success || result.errorCode != 0)
|
||||
continue;
|
||||
if (i >= static_cast<int>(result.distances.size()))
|
||||
continue;
|
||||
const auto& d = result.distances[static_cast<size_t>(i)];
|
||||
if (positiveFinite(d.distanceMm))
|
||||
values.push_back(static_cast<double>(d.distanceMm));
|
||||
}
|
||||
medians[static_cast<size_t>(i)] = medianValue(std::move(values));
|
||||
}
|
||||
return medians;
|
||||
}
|
||||
|
||||
double geometryStabilityScore(const DroneScrewResult& result,
|
||||
const std::vector<double>& medianHeights,
|
||||
const std::vector<double>& medianDistances)
|
||||
{
|
||||
double relErrorSum = 0.0;
|
||||
int compareCount = 0;
|
||||
|
||||
const int heightLimit = std::min(static_cast<int>(medianHeights.size()),
|
||||
static_cast<int>(result.boxes.size()));
|
||||
for (int i = 0; i < heightLimit; ++i)
|
||||
{
|
||||
const auto& b = result.boxes[static_cast<size_t>(i)];
|
||||
const double median = medianHeights[static_cast<size_t>(i)];
|
||||
if (!b.hasPhysicalHeight || !positiveFinite(b.physicalHeightMm) ||
|
||||
!positiveFinite(median))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const double denom = std::max(std::fabs(median), 1.0);
|
||||
relErrorSum += std::fabs(static_cast<double>(b.physicalHeightMm) - median) / denom;
|
||||
++compareCount;
|
||||
}
|
||||
|
||||
const int distanceLimit = std::min(static_cast<int>(medianDistances.size()),
|
||||
static_cast<int>(result.distances.size()));
|
||||
for (int i = 0; i < distanceLimit; ++i)
|
||||
{
|
||||
const auto& d = result.distances[static_cast<size_t>(i)];
|
||||
const double median = medianDistances[static_cast<size_t>(i)];
|
||||
if (!positiveFinite(d.distanceMm) || !positiveFinite(median))
|
||||
continue;
|
||||
const double denom = std::max(std::fabs(median), 1.0);
|
||||
relErrorSum += std::fabs(static_cast<double>(d.distanceMm) - median) / denom;
|
||||
++compareCount;
|
||||
}
|
||||
|
||||
if (compareCount <= 0)
|
||||
return 0.0;
|
||||
|
||||
const double avgRelError = relErrorSum / static_cast<double>(compareCount);
|
||||
return 1.0 / (1.0 + avgRelError * 10.0);
|
||||
}
|
||||
|
||||
double precisionResultRankScore(const DroneScrewResult& result,
|
||||
int expectedCount,
|
||||
double scoreThreshold,
|
||||
const std::vector<double>& medianHeights,
|
||||
const std::vector<double>& medianDistances)
|
||||
{
|
||||
const int expectedDistances = expectedDistanceCount(expectedCount);
|
||||
const int heightCount = physicalHeightCount(result);
|
||||
const int distanceCount = validDistanceCount(result);
|
||||
const int matchedCount = stereoMatchedPhysicalHeightCount(result);
|
||||
const int trustedStatusOkCount = trustedStatusOkPhysicalHeightCount(result);
|
||||
const int scoreOkCount = scoreOkPhysicalHeightCount(result, scoreThreshold);
|
||||
|
||||
const bool statusOk = result.success && result.errorCode == 0;
|
||||
const bool countComplete = heightCount == expectedCount;
|
||||
const bool distanceComplete = distanceCount == expectedDistances;
|
||||
const bool allMatched = countComplete && matchedCount == heightCount;
|
||||
const bool allTrustedStatusOk = countComplete && trustedStatusOkCount == heightCount;
|
||||
const bool allScoreOk = countComplete && scoreOkCount == heightCount;
|
||||
|
||||
const double heightRatio =
|
||||
expectedCount > 0
|
||||
? std::max(0.0, 1.0 - std::fabs(static_cast<double>(heightCount - expectedCount)) /
|
||||
static_cast<double>(expectedCount))
|
||||
: 1.0;
|
||||
const double distanceRatio =
|
||||
expectedDistances > 0
|
||||
? std::max(0.0, 1.0 - std::fabs(static_cast<double>(distanceCount - expectedDistances)) /
|
||||
static_cast<double>(expectedDistances))
|
||||
: 1.0;
|
||||
const double completeness = (heightRatio + distanceRatio) * 0.5;
|
||||
const double avgScore = averageDetectionScore(result);
|
||||
const double stability = geometryStabilityScore(result, medianHeights, medianDistances);
|
||||
|
||||
return (statusOk ? 1000000000.0 : 0.0) +
|
||||
(countComplete ? 100000000.0 : 0.0) +
|
||||
(distanceComplete ? 50000000.0 : 0.0) +
|
||||
(allMatched ? 10000000.0 : 0.0) +
|
||||
(allTrustedStatusOk ? 5000000.0 : 0.0) +
|
||||
(allScoreOk ? 1000000.0 : 0.0) +
|
||||
completeness * 10000.0 +
|
||||
avgScore * 1000.0 +
|
||||
stability * 100.0;
|
||||
}
|
||||
|
||||
QString savedImageFileName(unsigned long long index, const QString& prefix)
|
||||
{
|
||||
return QStringLiteral("%1_%2Image.png").arg(index).arg(prefix);
|
||||
@ -259,10 +512,13 @@ QString savedImageFileName(unsigned long long index, const QString& prefix)
|
||||
|
||||
bool saveDetectionResultText(const QString& dirPath,
|
||||
unsigned long long imageIndex,
|
||||
const DroneScrewResult& result)
|
||||
const DroneScrewResult& result,
|
||||
const QString& fileName = QString())
|
||||
{
|
||||
const QString filePath = QDir(dirPath).filePath(
|
||||
QStringLiteral("%1_result.txt").arg(imageIndex));
|
||||
const QString resultFileName =
|
||||
fileName.isEmpty() ? QStringLiteral("%1_result.txt").arg(imageIndex)
|
||||
: fileName;
|
||||
const QString filePath = QDir(dirPath).filePath(resultFileName);
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text))
|
||||
{
|
||||
@ -280,30 +536,43 @@ bool saveDetectionResultText(const QString& dirPath,
|
||||
out << "success:" << (result.success ? 1 : 0) << '\n';
|
||||
out << "errorCode:" << result.errorCode << '\n';
|
||||
out << "message:" << QString::fromStdString(result.message) << '\n';
|
||||
out << "isFinalResult:" << (result.isFinalResult ? 1 : 0) << '\n';
|
||||
out << "rankScore:" << QString::number(result.rankScore, 'f', 6) << '\n';
|
||||
out << "rankSampleCount:" << result.rankSampleCount << '\n';
|
||||
out << "saveIndex:" << static_cast<qulonglong>(result.saveIndex) << '\n';
|
||||
|
||||
int physicalHeightCount = 0;
|
||||
for (const auto& b : result.boxes)
|
||||
{
|
||||
if (b.hasPhysicalHeight)
|
||||
if (b.hasPhysicalHeight && positiveFinite(b.physicalHeightMm))
|
||||
++physicalHeightCount;
|
||||
}
|
||||
out << "physicalHeightCount:" << physicalHeightCount << '\n';
|
||||
out << "heightCount:" << physicalHeightCount << '\n';
|
||||
int physicalHeightIndex = 1;
|
||||
for (const auto& b : result.boxes)
|
||||
{
|
||||
if (!b.hasPhysicalHeight)
|
||||
if (!b.hasPhysicalHeight || !positiveFinite(b.physicalHeightMm))
|
||||
continue;
|
||||
out << "physicalHeight" << physicalHeightIndex << "Id:" << physicalHeightIndex << '\n';
|
||||
out << "physicalHeight" << physicalHeightIndex << "Mm:"
|
||||
out << "height" << physicalHeightIndex << "Id:" << physicalHeightIndex << '\n';
|
||||
out << "height" << physicalHeightIndex << "_mm:"
|
||||
<< QString::number(b.physicalHeightMm, 'f', 3) << '\n';
|
||||
++physicalHeightIndex;
|
||||
}
|
||||
|
||||
out << "rodSpacingCount:" << static_cast<int>(result.distances.size()) << '\n';
|
||||
int rodSpacingCount = 0;
|
||||
for (const auto& d : result.distances)
|
||||
{
|
||||
if (positiveFinite(d.distanceMm))
|
||||
++rodSpacingCount;
|
||||
}
|
||||
out << "rodSpacingCount:" << rodSpacingCount << '\n';
|
||||
int rodSpacingIndex = 1;
|
||||
for (size_t i = 0; i < result.distances.size(); ++i)
|
||||
{
|
||||
const auto& d = result.distances[i];
|
||||
const int index = static_cast<int>(i + 1);
|
||||
if (!positiveFinite(d.distanceMm))
|
||||
continue;
|
||||
const int index = rodSpacingIndex++;
|
||||
out << "rodSpacing" << index << "FromId:" << (d.fromId + 1) << '\n';
|
||||
out << "rodSpacing" << index << "ToId:" << (d.toId + 1) << '\n';
|
||||
out << "rodSpacing" << index << "Mm:"
|
||||
@ -1795,6 +2064,107 @@ QJsonObject DroneScrewServerPresenter::getCalibrationInfo() const
|
||||
return calibration;
|
||||
}
|
||||
|
||||
bool DroneScrewServerPresenter::getLastFinalDetectionResult(DroneScrewResult& result) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_detectionResultMutex);
|
||||
if (!m_hasLastFinalResult)
|
||||
return false;
|
||||
result = m_lastFinalResult;
|
||||
return true;
|
||||
}
|
||||
|
||||
void DroneScrewServerPresenter::resetPrecisionResultSamples()
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_detectionResultMutex);
|
||||
m_precisionResultSamples.clear();
|
||||
m_lastFinalResult = DroneScrewResult{};
|
||||
m_hasLastFinalResult = false;
|
||||
}
|
||||
|
||||
void DroneScrewServerPresenter::clearLastFinalDetectionResult()
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_detectionResultMutex);
|
||||
m_lastFinalResult = DroneScrewResult{};
|
||||
m_hasLastFinalResult = false;
|
||||
}
|
||||
|
||||
void DroneScrewServerPresenter::recordPrecisionResultSample(const DroneScrewResult& result,
|
||||
unsigned long long imageIndex)
|
||||
{
|
||||
if (imageIndex == 0)
|
||||
return;
|
||||
|
||||
DroneScrewResult stored = result;
|
||||
stored.saveIndex = imageIndex;
|
||||
stored.isFinalResult = false;
|
||||
|
||||
std::lock_guard<std::mutex> lk(m_detectionResultMutex);
|
||||
m_precisionResultSamples.push_back(PrecisionResultSample{stored, imageIndex});
|
||||
}
|
||||
|
||||
bool DroneScrewServerPresenter::selectBestPrecisionResult(DroneScrewResult& result,
|
||||
unsigned long long& imageIndex,
|
||||
double& rankScore,
|
||||
int& sampleCount) const
|
||||
{
|
||||
std::vector<PrecisionResultSample> samples;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_detectionResultMutex);
|
||||
samples = m_precisionResultSamples;
|
||||
}
|
||||
|
||||
sampleCount = static_cast<int>(samples.size());
|
||||
if (samples.empty())
|
||||
return false;
|
||||
|
||||
std::vector<DroneScrewResult> results;
|
||||
results.reserve(samples.size());
|
||||
for (const auto& sample : samples)
|
||||
results.push_back(sample.result);
|
||||
|
||||
const int expectedCount = expectedBoltCountOrDefault(m_algoParams.expectedBoltCount);
|
||||
const int distanceCount = expectedDistanceCount(expectedCount);
|
||||
const double scoreThreshold = std::max(0.0, static_cast<double>(m_algoParams.scoreThreshold));
|
||||
const std::vector<double> medianHeights =
|
||||
sessionMedianPhysicalHeights(results, expectedCount);
|
||||
const std::vector<double> medianDistances =
|
||||
sessionMedianDistances(results, distanceCount);
|
||||
|
||||
size_t bestIndex = 0;
|
||||
double bestScore = -std::numeric_limits<double>::infinity();
|
||||
for (size_t i = 0; i < samples.size(); ++i)
|
||||
{
|
||||
const double score = precisionResultRankScore(samples[i].result,
|
||||
expectedCount,
|
||||
scoreThreshold,
|
||||
medianHeights,
|
||||
medianDistances);
|
||||
if (score > bestScore ||
|
||||
(std::fabs(score - bestScore) < 1e-9 &&
|
||||
samples[i].imageIndex > samples[bestIndex].imageIndex))
|
||||
{
|
||||
bestScore = score;
|
||||
bestIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
result = samples[bestIndex].result;
|
||||
imageIndex = samples[bestIndex].imageIndex;
|
||||
rankScore = bestScore;
|
||||
result.isFinalResult = true;
|
||||
result.rankScore = bestScore;
|
||||
result.rankSampleCount = sampleCount;
|
||||
result.saveIndex = imageIndex;
|
||||
return true;
|
||||
}
|
||||
|
||||
void DroneScrewServerPresenter::setLastFinalDetectionResult(const DroneScrewResult& result)
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_detectionResultMutex);
|
||||
m_lastFinalResult = result;
|
||||
m_hasLastFinalResult = true;
|
||||
}
|
||||
|
||||
int DroneScrewServerPresenter::startDetectionWork()
|
||||
{
|
||||
if (m_bLiveStreaming.load())
|
||||
@ -1824,6 +2194,7 @@ int DroneScrewServerPresenter::startDetectionWork()
|
||||
return ERR_CODE(DRONESCREW_ERR_CAMERA_NOT_CONNECTED);
|
||||
}
|
||||
|
||||
resetPrecisionResultSamples();
|
||||
m_detectPipelineMode = kDetectPipelinePrecision;
|
||||
m_activeTriggerFps = static_cast<int>(kPrecisionFrameRate);
|
||||
|
||||
@ -1864,6 +2235,7 @@ int DroneScrewServerPresenter::stopDetectionWork()
|
||||
if (m_bLiveStreaming.load())
|
||||
{
|
||||
LOG_INFO("[DETECT] stop ignored: live stream keeps binocular detection running\n");
|
||||
clearLastFinalDetectionResult();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -1871,6 +2243,7 @@ int DroneScrewServerPresenter::stopDetectionWork()
|
||||
{
|
||||
stopGpioTriggerLoop();
|
||||
stopImageSaveThread();
|
||||
clearLastFinalDetectionResult();
|
||||
LOG_WARN("Detection not started\n");
|
||||
return 0;
|
||||
}
|
||||
@ -1879,7 +2252,46 @@ int DroneScrewServerPresenter::stopDetectionWork()
|
||||
m_bThreadExit = true;
|
||||
if (m_detectThread.joinable())
|
||||
m_detectThread.join();
|
||||
|
||||
DroneScrewResult finalResult;
|
||||
unsigned long long finalImageIndex = 0;
|
||||
double finalRankScore = 0.0;
|
||||
int finalSampleCount = 0;
|
||||
const bool hasFinalResult = selectBestPrecisionResult(finalResult,
|
||||
finalImageIndex,
|
||||
finalRankScore,
|
||||
finalSampleCount);
|
||||
QString finalDir;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_imageSaveMutex);
|
||||
finalDir = m_imageSaveSessionDir;
|
||||
}
|
||||
|
||||
stopImageSaveThread();
|
||||
if (hasFinalResult)
|
||||
{
|
||||
finalResult.isFinalResult = true;
|
||||
finalResult.rankScore = finalRankScore;
|
||||
finalResult.rankSampleCount = finalSampleCount;
|
||||
finalResult.saveIndex = finalImageIndex;
|
||||
if (!finalDir.isEmpty())
|
||||
saveDetectionResultText(finalDir, finalImageIndex, finalResult,
|
||||
QStringLiteral("final_result.txt"));
|
||||
setLastFinalDetectionResult(finalResult);
|
||||
LOG_INFO("[DETECT-final] selected index=%llu frame=%llu score=%.6f samples=%d boxes=%zu distances=%zu dir=%s\n",
|
||||
finalImageIndex,
|
||||
static_cast<unsigned long long>(finalResult.frameId),
|
||||
finalRankScore,
|
||||
finalSampleCount,
|
||||
finalResult.boxes.size(),
|
||||
finalResult.distances.size(),
|
||||
finalDir.toStdString().c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
clearLastFinalDetectionResult();
|
||||
LOG_WARN("[DETECT-final] no saved precision result samples\n");
|
||||
}
|
||||
m_bIsDetecting = false;
|
||||
m_detectPipelineMode = kDetectPipelinePrecision;
|
||||
m_activeTriggerFps = static_cast<int>(kPrecisionFrameRate);
|
||||
@ -2472,7 +2884,7 @@ void DroneScrewServerPresenter::stopImageSaveThread()
|
||||
}
|
||||
}
|
||||
|
||||
void DroneScrewServerPresenter::enqueueImageSave(const MvsImageData& leftImg,
|
||||
bool DroneScrewServerPresenter::enqueueImageSave(const MvsImageData& leftImg,
|
||||
const MvsImageData& rightImg,
|
||||
const DroneScrewResult& result,
|
||||
unsigned long long index)
|
||||
@ -2493,9 +2905,9 @@ void DroneScrewServerPresenter::enqueueImageSave(const MvsImageData& leftImg,
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_imageSaveMutex);
|
||||
if (m_imageSaveThreadExit.load() || m_imageSaveSessionDir.isEmpty())
|
||||
return;
|
||||
return false;
|
||||
if (dropIfQueueFull())
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto copyMono8 = [](const MvsImageData& src, ImageSaveBuffer& dst) -> bool {
|
||||
@ -2522,27 +2934,30 @@ void DroneScrewServerPresenter::enqueueImageSave(const MvsImageData& leftImg,
|
||||
ImageSaveJob job;
|
||||
job.index = index;
|
||||
job.result = result;
|
||||
job.result.isFinalResult = false;
|
||||
job.result.saveIndex = index;
|
||||
job.hasResult = true;
|
||||
if (!copyMono8(leftImg, job.left) || !copyMono8(rightImg, job.right))
|
||||
{
|
||||
LOG_WARN("[SAVE] skip image save: unsupported image pair index=%llu left=%ux%u pf=0x%x right=%ux%u pf=0x%x\n",
|
||||
index, leftImg.width, leftImg.height, leftImg.pixelFormat,
|
||||
rightImg.width, rightImg.height, rightImg.pixelFormat);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_imageSaveMutex);
|
||||
if (m_imageSaveThreadExit.load() || m_imageSaveSessionDir.isEmpty())
|
||||
return;
|
||||
return false;
|
||||
|
||||
if (dropIfQueueFull())
|
||||
return;
|
||||
return false;
|
||||
|
||||
job.dirPath = m_imageSaveSessionDir;
|
||||
m_imageSaveQueue.emplace_back(std::move(job));
|
||||
}
|
||||
m_imageSaveCv.notify_one();
|
||||
return true;
|
||||
}
|
||||
|
||||
void DroneScrewServerPresenter::imageSaveThreadFunc(int workerIndex)
|
||||
@ -2877,8 +3292,16 @@ std::string DroneScrewServerPresenter::detectMode() const
|
||||
|
||||
void DroneScrewServerPresenter::handleUpdateAlgoParams(const DroneScrewAlgoParams& params)
|
||||
{
|
||||
m_algoParams = params;
|
||||
if (m_pAlgo) m_pAlgo->UpdateParams(params);
|
||||
DroneScrewAlgoParams merged = m_algoParams;
|
||||
merged.scoreThreshold = params.scoreThreshold;
|
||||
merged.nmsThreshold = params.nmsThreshold;
|
||||
merged.inputWidth = params.inputWidth;
|
||||
merged.inputHeight = params.inputHeight;
|
||||
merged.modelType = params.modelType;
|
||||
merged.expectedBoltCount = expectedBoltCountOrDefault(params.expectedBoltCount);
|
||||
|
||||
m_algoParams = merged;
|
||||
if (m_pAlgo) m_pAlgo->UpdateParams(merged);
|
||||
saveConfiguration();
|
||||
}
|
||||
|
||||
@ -3192,7 +3615,11 @@ void DroneScrewServerPresenter::detectThreadFunc()
|
||||
result.boxes.size(), result.distances.size());
|
||||
|
||||
if (!distanceMode)
|
||||
enqueueImageSave(leftImg, rightImg, result, ++savedFrameCnt);
|
||||
{
|
||||
const unsigned long long saveIndex = ++savedFrameCnt;
|
||||
if (enqueueImageSave(leftImg, rightImg, result, saveIndex))
|
||||
recordPrecisionResultSample(result, saveIndex);
|
||||
}
|
||||
|
||||
emit detectionResult(result);
|
||||
|
||||
|
||||
@ -98,6 +98,7 @@ public:
|
||||
QString getRtspUrl() const { return m_rtspAdvertiseUrl; }
|
||||
QJsonObject getRuntimeInfo() const;
|
||||
QJsonObject getCalibrationInfo() const;
|
||||
bool getLastFinalDetectionResult(DroneScrewResult& result) const;
|
||||
int startDetectionWork();
|
||||
int stopDetectionWork();
|
||||
int startLiveStream();
|
||||
@ -237,11 +238,20 @@ private:
|
||||
void startImageSaveThread(const QString& modeName);
|
||||
void stopImageSaveThread();
|
||||
void imageSaveThreadFunc(int workerIndex);
|
||||
void enqueueImageSave(const MvsImageData& leftImg,
|
||||
bool enqueueImageSave(const MvsImageData& leftImg,
|
||||
const MvsImageData& rightImg,
|
||||
const DroneScrewResult& result,
|
||||
unsigned long long index);
|
||||
QString imageSaveModeName() const;
|
||||
void resetPrecisionResultSamples();
|
||||
void clearLastFinalDetectionResult();
|
||||
void recordPrecisionResultSample(const DroneScrewResult& result,
|
||||
unsigned long long imageIndex);
|
||||
bool selectBestPrecisionResult(DroneScrewResult& result,
|
||||
unsigned long long& imageIndex,
|
||||
double& rankScore,
|
||||
int& sampleCount) const;
|
||||
void setLastFinalDetectionResult(const DroneScrewResult& result);
|
||||
|
||||
private:
|
||||
struct ImageSaveBuffer
|
||||
@ -263,6 +273,12 @@ private:
|
||||
bool hasResult{false};
|
||||
};
|
||||
|
||||
struct PrecisionResultSample
|
||||
{
|
||||
DroneScrewResult result;
|
||||
unsigned long long imageIndex{0};
|
||||
};
|
||||
|
||||
// 双目相机设备
|
||||
IMvsDevice* m_pLeftCamera{nullptr};
|
||||
IMvsDevice* m_pRightCamera{nullptr};
|
||||
@ -354,6 +370,11 @@ private:
|
||||
std::deque<ImageSaveJob> m_imageSaveQueue;
|
||||
QString m_imageSaveSessionDir;
|
||||
unsigned long long m_imageSaveDropped{0};
|
||||
|
||||
mutable std::mutex m_detectionResultMutex;
|
||||
std::vector<PrecisionResultSample> m_precisionResultSamples;
|
||||
DroneScrewResult m_lastFinalResult;
|
||||
bool m_hasLastFinalResult{false};
|
||||
};
|
||||
|
||||
#endif // DRONESCREW_SERVER_PRESENTER_H
|
||||
|
||||
@ -152,6 +152,10 @@ void fillDetectionJson(const DroneScrewResult& result, QJsonObject& obj)
|
||||
obj["success"] = result.success;
|
||||
obj["errorCode"] = result.errorCode;
|
||||
obj["msg"] = QString::fromStdString(result.message);
|
||||
obj["isFinalResult"] = result.isFinalResult;
|
||||
obj["rankScore"] = result.rankScore;
|
||||
obj["rankSampleCount"] = result.rankSampleCount;
|
||||
obj["saveIndex"] = static_cast<qint64>(result.saveIndex);
|
||||
|
||||
QJsonArray boxes;
|
||||
for (const auto& b : result.boxes)
|
||||
@ -164,7 +168,17 @@ void fillDetectionJson(const DroneScrewResult& result, QJsonObject& obj)
|
||||
jb["w"] = b.width;
|
||||
jb["h"] = b.height;
|
||||
if (b.hasPhysicalHeight)
|
||||
jb["physicalHeightMm"] = static_cast<double>(b.physicalHeightMm);
|
||||
{
|
||||
const double heightMm = static_cast<double>(b.physicalHeightMm);
|
||||
jb["height_mm"] = heightMm;
|
||||
jb["hasStereoMatch"] = b.hasStereoMatch;
|
||||
jb["trusted"] = b.trusted;
|
||||
jb["matchConfidence"] = b.matchConfidence;
|
||||
jb["matchStatus"] = QString::fromStdString(b.matchStatus);
|
||||
jb["pairId"] = b.pairId;
|
||||
jb["leftIndex"] = b.leftIndex;
|
||||
jb["rightIndex"] = b.rightIndex;
|
||||
}
|
||||
boxes.append(jb);
|
||||
}
|
||||
obj["boxes"] = boxes;
|
||||
@ -468,7 +482,24 @@ QByteArray DroneScrewZmqProtocol::handleControlMessage(const QByteArray& reqData
|
||||
const int ret = m_stopWorkHandler ? m_stopWorkHandler()
|
||||
: fallbackHandlerError();
|
||||
if (ret != 0)
|
||||
{
|
||||
setErrorResp(resp, ret, "stop detection failed");
|
||||
}
|
||||
else if (m_finalDetectionResultProvider)
|
||||
{
|
||||
DroneScrewResult finalResult;
|
||||
if (m_finalDetectionResultProvider(finalResult))
|
||||
{
|
||||
finalResult.isFinalResult = true;
|
||||
fillDetectionJson(finalResult, resp);
|
||||
resp["hasFinalResult"] = true;
|
||||
publishDetectionResult(finalResult);
|
||||
}
|
||||
else
|
||||
{
|
||||
resp["hasFinalResult"] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (cmd == "start_stream")
|
||||
{
|
||||
|
||||
@ -72,6 +72,10 @@ public:
|
||||
{
|
||||
m_stopWorkHandler = std::move(handler);
|
||||
}
|
||||
void setFinalDetectionResultProvider(std::function<bool(DroneScrewResult&)> provider)
|
||||
{
|
||||
m_finalDetectionResultProvider = std::move(provider);
|
||||
}
|
||||
void setStartLiveStreamHandler(std::function<int()> handler)
|
||||
{
|
||||
m_startLiveStreamHandler = std::move(handler);
|
||||
@ -139,6 +143,7 @@ private:
|
||||
std::function<DroneScrewResult()> m_singleDetectionHandler;
|
||||
std::function<int()> m_startWorkHandler;
|
||||
std::function<int()> m_stopWorkHandler;
|
||||
std::function<bool(DroneScrewResult&)> m_finalDetectionResultProvider;
|
||||
std::function<int()> m_startLiveStreamHandler;
|
||||
std::function<int()> m_stopLiveStreamHandler;
|
||||
std::function<int()> m_swapCameraRolesHandler;
|
||||
|
||||
@ -8,6 +8,6 @@
|
||||
#define DRONESCREWSERVER_COMPANY_NAME "VisionTech"
|
||||
#define DRONESCREWSERVER_COPYRIGHT "Copyright (C) 2026"
|
||||
#define DRONESCREWSERVER_VERSION_STRING "1.0.0"
|
||||
#define DRONESCREWSERVER_VERSION_BUILD "4"
|
||||
#define DRONESCREWSERVER_VERSION_BUILD "5"
|
||||
|
||||
#endif // DRONESCREW_VERSION_H
|
||||
|
||||
@ -348,6 +348,9 @@ int main(int argc, char* argv[])
|
||||
zmq.setStopWorkHandler([&presenter]() {
|
||||
return presenter.stopDetectionWork();
|
||||
});
|
||||
zmq.setFinalDetectionResultProvider([&presenter](DroneScrewResult& result) {
|
||||
return presenter.getLastFinalDetectionResult(result);
|
||||
});
|
||||
zmq.setStartLiveStreamHandler([&presenter]() {
|
||||
return presenter.startLiveStream();
|
||||
});
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
# ABI fingerprint of libstereo_bolt.so
|
||||
# Your board MUST match these or the .so will not load / not run.
|
||||
# generated: 2026-07-01 (rebuilt on RK3588; OBB-axis ranging stability fix)
|
||||
# generated: 2026-07-02 (rebuilt on RK3588; v5 flat-top endpoint + 0703 OBB model; C ABI unchanged)
|
||||
# libstereo_bolt.so SHA256:
|
||||
# 67d9406c1ab69c36b8f3b363fe6cf38567fdac97c25fa160e4c80a4d63b172ae
|
||||
# 8fb1cf6dfae4f655da7e96ea50d858b3971674cb6676dff0b3873a2da250b9af
|
||||
|
||||
[arch]
|
||||
AArch64
|
||||
|
||||
@ -1,6 +1,55 @@
|
||||
CHANGELOG — libstereo_bolt delivery package
|
||||
============================================
|
||||
|
||||
## 2026-07-02 v5 — flat-top endpoint + 0703 OBB model
|
||||
|
||||
Rebuilt on RK3588 from source eb96ff4 (branch qc/axis-dir-consensus).
|
||||
C ABI unchanged (same 9 exported symbols) — no consumer recompile needed,
|
||||
only replace lib/ + weights/ + config/.
|
||||
|
||||
Changes vs v4:
|
||||
- Model: best_obb_1024_0703_20260702.rknn (0703 annotated finetune; far/oblique
|
||||
recall recovered — 0702 far scene 0-detect frames 131/167 -> ~0).
|
||||
- config.rk3588.12mp.yaml: model_path -> 0703 rknn, imgsz 1024, task obb,
|
||||
measurement.top_morphology: "flat_top" (ellipse-center tip + tight foot band
|
||||
for black flat-top studs), 0621 calib (stereo_calib_20260621_202857).
|
||||
- Ranging: physical-length consensus pairing repair + graded confidence.
|
||||
Board-verified on 0702 12MP: 8/8 bolts, status=ok, per-bolt 1.5-2.2m coherent.
|
||||
- Board timing (12MP single frame, steady state): ~450-475ms total
|
||||
(yolo ~180ms, cmatch ~115ms, rectify ~90ms, measure+so+qc ~65ms, plane
|
||||
skipped in height_from_plane:false).
|
||||
- NOTE: flat-top height ACCURACY still under validation (single-angle 10-frame
|
||||
board test median 150.6mm vs 160 nominal; needs multi-angle aggregation +
|
||||
on-site caliper zero-point before precision sign-off). Ranging + timing are
|
||||
production-ready; precise height functional but accuracy-pending.
|
||||
|
||||
## 2026-07-01 v4 — precise partial-output contract fix
|
||||
|
||||
Current delivery contents:
|
||||
- Model: best_obb_1024.rknn (OBB, imgsz 1024, RKNN)
|
||||
- Precise measurement and ranging share the same OBB model.
|
||||
- Runtime libraries are bundled in lib/: libstereo_bolt.so, OpenCV 4.5.0, librknnrt.so.
|
||||
- No target-board apt install is required for runtime OpenCV.
|
||||
|
||||
Behavior change:
|
||||
- Precise measurement no longer exports left-only fallback heights as product
|
||||
results. If a bolt is detected in the left image but has no right detection
|
||||
/ right ROI (`idx_R < 0`), it stays in the failure diagnostics only.
|
||||
- `data.bolts[]` now contains only complete right-paired bolts that measured
|
||||
cleanly. This avoids misleading fake heights when the right YOLO detector
|
||||
misses part of the expected bolt set.
|
||||
- C ABI is unchanged: function signatures and struct layouts are unchanged.
|
||||
|
||||
Performance / validation:
|
||||
- RK3588, precision_153313_954, 7 full-resolution frames, expected=4:
|
||||
every frame correctly returns `success=0`, `reason=count_mismatch`,
|
||||
`L=4`, `R=2`, `pairs=4`, `data.n_bolts=2`. The two missing-right bolts are
|
||||
not exported as heights.
|
||||
- RK3588, distance_153304_403, 21 binned frames:
|
||||
found=21/21, distance mean=1652.360 mm, min=1652.091, max=1652.582,
|
||||
elapsed mean=156.493 ms.
|
||||
- Windows ctest: 23/23 pass, including the right-eye-missing fallback guard.
|
||||
|
||||
## 2026-07-01 v3 — stable OBB-axis ranging summary
|
||||
|
||||
Current delivery contents:
|
||||
|
||||
@ -58,7 +58,7 @@ StereoBoltParamsC sb_default_params(void); /* 预填默认值,再覆盖你
|
||||
|---|---|
|
||||
| `lib/libstereo_bolt.so` | 预编译共享库(aarch64,含 `sb_create_ex` 内存接口) |
|
||||
| `include/stereo_bolt/c_api.h` | C ABI 头文件(唯一对外接口) |
|
||||
| `weights/best_obb_1024.rknn` | NPU 模型 —— OBB 检测(精测+测距共用,imgsz 1024,FP16) |
|
||||
| `weights/best_obb_1024_0703_20260702.rknn` | NPU 模型 —— OBB 检测(精测+测距共用,imgsz 1024,FP16) |
|
||||
| `calib/stereo_calib_20260621_202857_ascii.xml` | 12MP 双目标定(**0621 装配 baseline 482mm**,Halcon 位姿已修正) |
|
||||
| `config/config.rk3588.12mp.yaml` | 仅 `sb_create`(兼容)用 + 作参数参考;**`sb_create_ex` 不需要它** |
|
||||
| `lib/*.so` | 算法库、OpenCV 4.5.0 运行库、RKNN 运行时(aarch64 2.3.2),运行时通过 `LD_LIBRARY_PATH=./lib` 加载 |
|
||||
@ -130,8 +130,8 @@ void sb_free_bolt_module_result(StereoBoltModuleResultC* out); /* 用完必须
|
||||
|
||||
**坐标系/单位**:高度、间距、3D 坐标 = **mm**,**校正后左相机坐标系**(x 右 / y 下 / z=深度,典型 z≈1600–2200mm)。
|
||||
|
||||
**★ 部分输出契约(去 all-or-nothing)**:即使 `success==0`(数量不符 `count_mismatch`,无论检到的多还是少于 `expected`),
|
||||
`data.bolts[]` 仍含**所有干净测出的螺栓**(各带 height/top/foot/confidence);`failure`(reason + 完整 ROI + 配对状态)始终填充,能看出缺哪颗、为何缺。**别只看返回码就丢结果——照样遍历 `data.bolts[]`,按 `confidence` 自行取舍。**
|
||||
**★ 部分输出契约(但不导出假高度)**:即使 `success==0`(数量不符 `count_mismatch`,无论检到的多还是少于 `expected`),
|
||||
`data.bolts[]` 只包含**完整右目配对且测量干净的螺栓**(各带 height/top/foot/confidence)。`idx_R < 0` 的 left-only fallback 不作为产品高度输出,避免误导。`failure`(reason + 完整 ROI + 配对状态)始终填充,能看出缺哪颗、为何缺。
|
||||
|
||||
**最简用法(`sb_create_ex`)**:
|
||||
```c
|
||||
@ -200,7 +200,8 @@ sb_status_t sb_range_bolt_binned(
|
||||
| 板上吞吐@1024 | **约 400ms/帧** | **约 153ms/帧 ≈ 6.5Hz** |
|
||||
| 定位 | 触发式"测得准" | 无人机实时定位反馈 |
|
||||
|
||||
> 测距实测于 RK3588(best_obb_1024.rknn,distance_153304_403,21 帧):均值 153.146ms,距离均值 1652.360mm,跨度 0.491mm。
|
||||
> 测距历史基准于 RK3588(v4 旧模型 best_obb_1024.rknn,distance_153304_403,21 帧):均值 153.146ms,距离均值 1652.360mm,跨度 0.491mm。
|
||||
> v5(0703 模型)板上测距复核 0702-12mp:8/8 螺杆 status=ok,逐杆距离 1.5–2.2m 连贯;整帧 ~450–475ms。
|
||||
> 本交付为单模型 OBB@1024(精测+测距共用)。
|
||||
|
||||
---
|
||||
@ -217,10 +218,10 @@ cmake --build build # -> bolt_demo(精测)+ range_demo(测距)
|
||||
# 精测:传标定 XML + 模型 + 左右图 + 期望螺栓数
|
||||
LD_LIBRARY_PATH=../lib:$LD_LIBRARY_PATH \
|
||||
./build/bolt_demo ../calib/stereo_calib_20260621_202857_ascii.xml \
|
||||
../weights/best_obb_1024.rknn L.bmp R.bmp 6
|
||||
../weights/best_obb_1024_0703_20260702.rknn L.bmp R.bmp 6
|
||||
|
||||
# 测距:传标定 + 模型 + 左右图(demo 内部 2×2 binning;生产直接喂相机 binned buffer)
|
||||
LD_LIBRARY_PATH=../lib:$LD_LIBRARY_PATH \
|
||||
./build/range_demo ../calib/stereo_calib_20260621_202857_ascii.xml \
|
||||
../weights/best_obb_1024.rknn L.bmp R.bmp
|
||||
../weights/best_obb_1024_0703_20260702.rknn L.bmp R.bmp
|
||||
```
|
||||
|
||||
@ -34,7 +34,7 @@ cd stereo_bolt_delivery
|
||||
stereo_bolt_delivery/
|
||||
├── lib/ ← 算法库 + OpenCV 4.5.0 + librknnrt(共 9 个 .so)
|
||||
├── include/stereo_bolt/ ← c_api.h(唯一对外头文件)
|
||||
├── weights/ ← best_obb_1024.rknn(NPU OBB 模型)
|
||||
├── weights/ ← best_obb_1024_0703_20260702.rknn(NPU OBB 模型)
|
||||
├── calib/ ← 出厂标定 XML(0621 装配,baseline 482mm)
|
||||
├── config/ ← config.rk3588.12mp.yaml(兼容接口用)
|
||||
├── example/ ← 最小 C 示例 + CMakeLists
|
||||
@ -93,7 +93,7 @@ StereoBoltCtx* ctx = sb_create_ex(&calib, &model, &p); // 只调一次(慢,
|
||||
```
|
||||
|
||||
> 兼容方式:`sb_create("config/config.rk3588.12mp.yaml")`(读文件,需工作目录正确)。
|
||||
> 模型:`weights/best_obb_1024.rknn`(精测 + 测距共用,OBB imgsz 1024)。
|
||||
> 模型:`weights/best_obb_1024_0703_20260702.rknn`(精测 + 测距共用,OBB imgsz 1024)。
|
||||
> 标定:`calib/stereo_calib_20260621_202857_ascii.xml`(0621 装配,baseline 482 mm)。
|
||||
|
||||
### 高度测量模式
|
||||
|
||||
@ -145,6 +145,7 @@ bolt:
|
||||
measurement:
|
||||
top_extraction_method: "percentile_on_axis"
|
||||
top_percentile: 0.99
|
||||
top_morphology: "flat_top"
|
||||
height_from_plane: false
|
||||
plane_use_sgbm: true
|
||||
plane_sgbm_scale: 0.5
|
||||
@ -161,7 +162,7 @@ yolo:
|
||||
enabled: true
|
||||
model_path: "weights/best.rknn"
|
||||
backend: "rknn"
|
||||
task: "auto"
|
||||
task: "obb"
|
||||
imgsz: 1024
|
||||
conf: 0.5
|
||||
dead_zone_min_valid_pts: 50
|
||||
|
||||
@ -108,11 +108,12 @@ typedef struct {
|
||||
|
||||
typedef struct {
|
||||
int success; /* 1 == all `expected` bolts measured ok */
|
||||
/* Partial-result contract: `data.bolts` holds every bolt that measured cleanly
|
||||
* (each with its own height/status/confidence) EVEN when success == 0 -- e.g. on
|
||||
* count_mismatch you still get the bolts that did measure. `failure` is always
|
||||
* populated too (reason + full left/right ROIs + per-pair status) so you can see
|
||||
* which bolts are missing and why. When success == 1, failure.reason is empty. */
|
||||
/* Partial-result contract: `data.bolts` holds complete right-paired bolts that
|
||||
* measured cleanly (each with its own height/status/confidence) EVEN when
|
||||
* success == 0. Left-only fallback heights (idx_R < 0) are not exported as
|
||||
* product measurements; `failure` is always populated too (reason + full
|
||||
* left/right ROIs + per-pair status) so you can see which bolts are missing
|
||||
* and why. When success == 1, failure.reason is empty. */
|
||||
StereoBoltModuleSuccessC data;
|
||||
StereoBoltModuleFailureC failure;
|
||||
} StereoBoltModuleResultC;
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -57,6 +57,13 @@ struct WDRemoteDetectionBox
|
||||
int height{0};
|
||||
bool hasPhysicalHeight{false};
|
||||
float physicalHeightMm{0.0f};
|
||||
bool hasStereoMatch{false};
|
||||
bool trusted{false};
|
||||
int matchConfidence{1};
|
||||
std::string matchStatus;
|
||||
int pairId{-1};
|
||||
int leftIndex{-1};
|
||||
int rightIndex{-1};
|
||||
};
|
||||
|
||||
/**
|
||||
@ -79,6 +86,10 @@ struct WDRemoteDetectionFrame
|
||||
bool success{true};
|
||||
int errorCode{0};
|
||||
std::string message;
|
||||
bool isFinalResult{false};
|
||||
double rankScore{0.0};
|
||||
int rankSampleCount{0};
|
||||
uint64_t saveIndex{0};
|
||||
std::vector<WDRemoteDetectionBox> boxes;
|
||||
std::vector<WDRemoteDetectionDistance> distances;
|
||||
};
|
||||
@ -92,8 +103,8 @@ struct WDRemoteAlgoParams
|
||||
float nmsThreshold{0.45f};
|
||||
int inputWidth{640};
|
||||
int inputHeight{640};
|
||||
std::string modelPath;
|
||||
int modelType{0}; // 0=YOLO, 1=自定义
|
||||
int expectedBoltCount{8};
|
||||
};
|
||||
|
||||
struct WDRemoteMatrix
|
||||
@ -177,6 +188,7 @@ struct WDRemoteServerInfo
|
||||
int algoHeight{640};
|
||||
std::string algoModel;
|
||||
int algoModelType{0};
|
||||
int algoExpectedBoltCount{8};
|
||||
WDRemoteCalibrationInfo calibration;
|
||||
std::string rawJson; // 原始 JSON 应答,方便扩展字段
|
||||
};
|
||||
|
||||
@ -306,6 +306,7 @@ void fillRuntimeFields(const Json::Value& root, WDRemoteServerInfo& info)
|
||||
info.algoHeight = root.get("algoHeight", 640).asInt();
|
||||
info.algoModel = root.get("algoModel", "").asString();
|
||||
info.algoModelType = root.get("algoModelType", 0).asInt();
|
||||
info.algoExpectedBoltCount = root.get("algoExpectedBoltCount", 8).asInt();
|
||||
fillCalibrationInfo(root, info.calibration);
|
||||
}
|
||||
|
||||
@ -829,6 +830,17 @@ int WDRemoteReceiver::StopWork(WDRemoteWorkMode mode, int timeoutMs)
|
||||
const int detectCode = responseErrorCode(detectResp);
|
||||
if (detectCode != 0) return detectCode;
|
||||
|
||||
WDRemoteDetectionFrame finalFrame;
|
||||
if (parseDetectionJson(detectResp, finalFrame) && finalFrame.isFinalResult)
|
||||
{
|
||||
DetectionCallback cb;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(m_mtxCallback);
|
||||
cb = m_cbDetection;
|
||||
}
|
||||
if (cb) cb(finalFrame);
|
||||
}
|
||||
|
||||
if (m_workState == WorkState::Detection)
|
||||
{
|
||||
StopSubscribeRawImage();
|
||||
@ -881,8 +893,8 @@ int WDRemoteReceiver::SetAlgoParams(const WDRemoteAlgoParams& params, int timeou
|
||||
req["nms"] = params.nmsThreshold;
|
||||
req["width"] = params.inputWidth;
|
||||
req["height"] = params.inputHeight;
|
||||
req["model"] = params.modelPath;
|
||||
req["modelType"] = params.modelType;
|
||||
req["expectedBoltCount"] = params.expectedBoltCount;
|
||||
Json::FastWriter w;
|
||||
std::string resp;
|
||||
int r = sendCommandAndParse(w.write(req), resp, timeoutMs);
|
||||
@ -1279,6 +1291,11 @@ bool WDRemoteReceiver::parseDetectionJson(const std::string& jsonStr,
|
||||
outFrame.success = root.get("success", true).asBool();
|
||||
outFrame.errorCode = root.get("errorCode", 0).asInt();
|
||||
outFrame.message = root.get("msg", "").asString();
|
||||
outFrame.isFinalResult = root.get("isFinalResult",
|
||||
root.get("resultFinal", false)).asBool();
|
||||
outFrame.rankScore = root.get("rankScore", 0.0).asDouble();
|
||||
outFrame.rankSampleCount = root.get("rankSampleCount", 0).asInt();
|
||||
outFrame.saveIndex = static_cast<uint64_t>(root.get("saveIndex", 0).asInt64());
|
||||
|
||||
outFrame.boxes.clear();
|
||||
outFrame.distances.clear();
|
||||
@ -1297,8 +1314,15 @@ bool WDRemoteReceiver::parseDetectionJson(const std::string& jsonStr,
|
||||
box.y = b.get("y", 0 ).asInt();
|
||||
box.width = b.get("w", 0 ).asInt();
|
||||
box.height = b.get("h", 0 ).asInt();
|
||||
box.hasPhysicalHeight = b.isMember("physicalHeightMm");
|
||||
box.physicalHeightMm = static_cast<float>(b.get("physicalHeightMm", 0.0).asDouble());
|
||||
box.hasPhysicalHeight = b.isMember("height_mm");
|
||||
box.physicalHeightMm = static_cast<float>(b.get("height_mm", 0.0).asDouble());
|
||||
box.hasStereoMatch = b.get("hasStereoMatch", false).asBool();
|
||||
box.trusted = b.get("trusted", false).asBool();
|
||||
box.matchConfidence = b.get("matchConfidence", 1).asInt();
|
||||
box.matchStatus = b.get("matchStatus", "").asString();
|
||||
box.pairId = b.get("pairId", -1).asInt();
|
||||
box.leftIndex = b.get("leftIndex", -1).asInt();
|
||||
box.rightIndex = b.get("rightIndex", -1).asInt();
|
||||
outFrame.boxes.push_back(box);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user