螺栓检测结果列表更新

This commit is contained in:
杰仔 2026-07-03 14:19:06 +08:00
parent 81a5298e14
commit a6d3bd240d
38 changed files with 1501 additions and 76 deletions

View File

@ -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 += \

View File

@ -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)

View File

@ -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;

View File

@ -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());

View File

@ -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; }

View File

@ -19,7 +19,7 @@
inputWidth="640"
inputHeight="640"
modelType="0"
modelPath="" />
expectedBoltCount="8" />
<!-- 显示选项 -->
<Display drawBoxes="true"

View File

@ -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)

View File

@ -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;

View File

@ -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>

View File

@ -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();

View File

@ -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;

View 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();
}

View 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

View 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>

View File

@ -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();
}
// ======================== 页面布局 ========================

View File

@ -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

View File

@ -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>

View File

@ -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")

View File

@ -31,7 +31,7 @@ struct DroneScrewAlgoUiParams
int inputWidth{640};
int inputHeight{640};
int modelType{0};
std::string modelPath;
int expectedBoltCount{8};
};
/**

View File

@ -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");

View File

@ -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);
}
}

View File

@ -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};
};
/**

View File

@ -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);

View File

@ -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

View File

@ -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")
{

View File

@ -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;

View File

@ -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

View File

@ -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();
});

View File

@ -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

View File

@ -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:

View File

@ -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 1024FP16 |
| `weights/best_obb_1024_0703_20260702.rknn` | NPU 模型 —— OBB 检测(精测+测距共用imgsz 1024FP16 |
| `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≈16002200mm
**★ 部分输出契约(去 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** |
| 定位 | 触发式"测得准" | 无人机实时定位反馈 |
> 测距实测于 RK3588best_obb_1024.rknndistance_153304_40321 帧):均值 153.146ms,距离均值 1652.360mm,跨度 0.491mm。
> 测距历史基准于 RK3588v4 旧模型 best_obb_1024.rknndistance_153304_40321 帧):均值 153.146ms,距离均值 1652.360mm,跨度 0.491mm。
> v50703 模型)板上测距复核 0702-12mp8/8 螺杆 status=ok逐杆距离 1.52.2m 连贯;整帧 ~450475ms。
> 本交付为单模型 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
```

View File

@ -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.rknnNPU OBB 模型)
├── weights/ ← best_obb_1024_0703_20260702.rknnNPU OBB 模型)
├── calib/ ← 出厂标定 XML0621 装配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
### 高度测量模式

View File

@ -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

View File

@ -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;

View File

@ -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 应答,方便扩展字段
};

View File

@ -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);
}
}