螺栓的更新算法

This commit is contained in:
杰仔 2026-07-01 18:16:18 +08:00
parent 00de98b6be
commit d3e6b4d0f3
24 changed files with 891 additions and 395 deletions

View File

@ -39,6 +39,7 @@ public:
void SetStatusReceiver(IYDroneScrewCtrlStatus* receiver) { m_pReceiver = receiver; }
void ApplyConfig(const DroneScrewCtrlConfigResult& cfg);
void UpdateDisplayOptions(const DroneScrewDisplayOption& options);
int InitApp();
void DeinitApp();

View File

@ -181,6 +181,11 @@ void DroneScrewCtrlPresenter::ApplyConfig(const DroneScrewCtrlConfigResult& cfg)
}
}
void DroneScrewCtrlPresenter::UpdateDisplayOptions(const DroneScrewDisplayOption& options)
{
m_cfg.display = options;
}
int DroneScrewCtrlPresenter::InitApp()
{
if (m_bRunning.load()) return 0;

View File

@ -3,8 +3,8 @@
#define DRONESCREWCTRL_APP_NAME "无人机螺杆控制端"
#define DRONESCREWCTRL_VERSION_STRING "1.0.0"
#define DRONESCREWCTRL_BUILD_STRING "2"
#define DRONESCREWCTRL_FULL_VERSION_STRING "V1.0.0_2"
#define DRONESCREWCTRL_BUILD_STRING "3"
#define DRONESCREWCTRL_FULL_VERSION_STRING "V1.0.0_3"
inline const char* GetDroneScrewCtrlFullVersion() { return DRONESCREWCTRL_FULL_VERSION_STRING; }

View File

@ -1,8 +1,11 @@
#include "dialogalgoarg.h"
#include "ui_dialogalgoarg.h"
#include <QCheckBox>
#include <QDoubleValidator>
#include <QFormLayout>
#include <QIntValidator>
#include <QLabel>
DialogAlgoArg::DialogAlgoArg(QWidget* parent)
: QDialog(parent)
@ -17,6 +20,7 @@ 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));
createDisplayRows();
}
DialogAlgoArg::~DialogAlgoArg()
@ -35,6 +39,17 @@ DroneScrewAlgoUiParams DialogAlgoArg::GetParams() const
return m_params;
}
void DialogAlgoArg::SetDisplayOptions(const DroneScrewDisplayOption& options)
{
m_displayOptions = options;
loadDisplayToUi(options);
}
DroneScrewDisplayOption DialogAlgoArg::GetDisplayOptions() const
{
return m_displayOptions;
}
void DialogAlgoArg::loadToUi(const DroneScrewAlgoUiParams& p)
{
if (ui->edit_score) ui->edit_score->setText(QString::number(p.scoreThreshold, 'f', 2));
@ -57,9 +72,39 @@ DroneScrewAlgoUiParams DialogAlgoArg::collectFromUi() const
return p;
}
void DialogAlgoArg::createDisplayRows()
{
if (!ui->fl_main)
return;
m_checkDrawBoxes = new QCheckBox(QStringLiteral("显示"), this);
m_checkDrawBoxes->setObjectName(QStringLiteral("check_drawBoxes"));
m_checkDrawBoxes->setMinimumHeight(40);
m_checkDrawBoxes->setStyleSheet(QStringLiteral(
"QCheckBox { color: rgb(239, 241, 245); font-size: 18px; background: none; }"));
ui->fl_main->addRow(new QLabel(QStringLiteral("显示结果框"), this),
m_checkDrawBoxes);
}
void DialogAlgoArg::loadDisplayToUi(const DroneScrewDisplayOption& options)
{
if (m_checkDrawBoxes)
m_checkDrawBoxes->setChecked(options.drawBoxes);
}
DroneScrewDisplayOption DialogAlgoArg::collectDisplayFromUi() const
{
DroneScrewDisplayOption options = m_displayOptions;
if (m_checkDrawBoxes)
options.drawBoxes = m_checkDrawBoxes->isChecked();
return options;
}
void DialogAlgoArg::on_btn_ok_clicked()
{
m_params = collectFromUi();
m_displayOptions = collectDisplayFromUi();
accept();
}
@ -71,10 +116,14 @@ void DialogAlgoArg::on_btn_cancel_clicked()
void DialogAlgoArg::on_btn_apply_clicked()
{
m_params = collectFromUi();
m_displayOptions = collectDisplayFromUi();
}
void DialogAlgoArg::on_btn_reset_clicked()
{
DroneScrewAlgoUiParams p; // 默认值
loadToUi(p);
DroneScrewDisplayOption options = m_displayOptions;
options.drawBoxes = true;
loadDisplayToUi(options);
}

View File

@ -5,6 +5,7 @@
#include "IVrConfig.h"
namespace Ui { class DialogAlgoArg; }
class QCheckBox;
class DialogAlgoArg : public QDialog
{
@ -16,6 +17,8 @@ public:
void SetParams(const DroneScrewAlgoUiParams& params);
DroneScrewAlgoUiParams GetParams() const;
void SetDisplayOptions(const DroneScrewDisplayOption& options);
DroneScrewDisplayOption GetDisplayOptions() const;
private slots:
void on_btn_ok_clicked();
@ -26,10 +29,15 @@ private slots:
private:
void loadToUi(const DroneScrewAlgoUiParams& p);
DroneScrewAlgoUiParams collectFromUi() const;
void createDisplayRows();
void loadDisplayToUi(const DroneScrewDisplayOption& options);
DroneScrewDisplayOption collectDisplayFromUi() const;
private:
Ui::DialogAlgoArg* ui{nullptr};
DroneScrewAlgoUiParams m_params;
DroneScrewDisplayOption m_displayOptions;
QCheckBox* m_checkDrawBoxes{nullptr};
};
#endif // DRONESCREWCTRL_DIALOG_ALGOARG_H

View File

@ -671,12 +671,16 @@ void MainWindow::on_btn_algo_config_clicked()
{
dlg.SetParams(cfg.algo);
}
dlg.SetDisplayOptions(cfg.display);
if (dlg.exec() == QDialog::Accepted)
{
cfg.algo = dlg.GetParams();
cfg.display = dlg.GetDisplayOptions();
ConfigManager::Instance().SetConfig(cfg);
ConfigManager::Instance().Save();
m_maxResultItems = cfg.display.maxResultListItems;
if (m_pPresenter) m_pPresenter->UpdateDisplayOptions(cfg.display);
if (m_pPresenter) m_pPresenter->PushAlgoParams(cfg.algo);
appendLog(QStringLiteral("已应用算法参数"));
}

View File

@ -9,9 +9,9 @@ namespace
void setResultTextStyle(Ui::ResultItem* ui)
{
const QString titleStyle =
QStringLiteral("color: rgb(239, 241, 245); background-color: rgb(37, 38, 42); font-size: 16px; font-weight: 600;");
QStringLiteral("color: rgb(239, 241, 245); background-color: rgb(37, 38, 42); font-size: 20px; font-weight: 600;");
const QString valueStyle =
QStringLiteral("color: rgb(255, 255, 255); background-color: rgb(37, 38, 42); font-size: 20px; font-weight: 700;");
QStringLiteral("color: rgb(255, 255, 255); background-color: rgb(37, 38, 42); font-size: 26px; font-weight: 700;");
if (ui->lb_id_t) ui->lb_id_t->setStyleSheet(titleStyle);
if (ui->lb_time_t) ui->lb_time_t->setStyleSheet(titleStyle);
@ -34,9 +34,7 @@ QString failedText(const CtrlDetectionFrame& frame)
QString oneDistanceText(const CtrlDetectionDistance& d)
{
const double meters = d.distanceMm / 1000.0;
if (d.fromId < 0)
return QStringLiteral("整体距离: %1m").arg(meters, 0, 'f', 3);
return QStringLiteral("目标%1距离: %2m").arg(d.fromId + 1).arg(meters, 0, 'f', 3);
return QStringLiteral("%1m").arg(meters, 0, 'f', 3);
}
QString distanceText(const CtrlDetectionFrame& frame, int targetIndex)
@ -58,7 +56,7 @@ 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);
return QString::number(d.toId + 1);
}
return QStringLiteral("-");
}
@ -113,7 +111,14 @@ void ResultItem::setResultData(int targetIndex,
{
const bool precisionMode = (mode == DisplayMode::Precision);
setResultTextStyle(ui);
setMinimumHeight(precisionMode ? 92 : 82);
setMinimumHeight(precisionMode ? 104 : 108);
if (ui->lb_id_t)
ui->lb_id_t->setText(QStringLiteral("ID"));
if (ui->lb_status_t)
ui->lb_status_t->setText(mode == DisplayMode::Distance
? QStringLiteral("距离")
: QStringLiteral("状态"));
if (ui->label_id)
{

View File

@ -15,9 +15,13 @@
#include <algorithm>
#include <cmath>
#include <cstring>
#include <exception>
#include <limits>
#include <sstream>
#ifdef DRONESCREW_STEREO_BOLT_DIRECT_LINK
#include <opencv2/calib3d.hpp>
#include <opencv2/core.hpp>
namespace
{
@ -28,6 +32,65 @@ constexpr int kDefaultYoloImgSize = 960;
constexpr int kDefaultExpectedBoltCount = 8;
constexpr int kMaxRangeBolts = 64;
cv::Mat mat64(int rows, int cols, const double* values)
{
cv::Mat m(rows, cols, CV_64F);
std::memcpy(m.ptr<double>(0), values, sizeof(double) * rows * cols);
return m;
}
int roiSampleCount(double span)
{
if (span <= 1.0)
return 2;
return std::max(3, std::min(17, static_cast<int>(std::ceil(span / 64.0)) + 1));
}
bool sampleMapBilinear(const std::vector<float>& mapX,
const std::vector<float>& mapY,
int mapWidth,
int mapHeight,
double rectX,
double rectY,
double& rawX,
double& rawY)
{
if (mapWidth <= 0 || mapHeight <= 0 ||
mapX.size() != static_cast<size_t>(mapWidth) * static_cast<size_t>(mapHeight) ||
mapY.size() != mapX.size())
{
return false;
}
rectX = std::max(0.0, std::min(rectX, static_cast<double>(mapWidth - 1)));
rectY = std::max(0.0, std::min(rectY, static_cast<double>(mapHeight - 1)));
const int x0 = static_cast<int>(std::floor(rectX));
const int y0 = static_cast<int>(std::floor(rectY));
const int x1 = std::min(x0 + 1, mapWidth - 1);
const int y1 = std::min(y0 + 1, mapHeight - 1);
const double tx = rectX - x0;
const double ty = rectY - y0;
const size_t i00 = static_cast<size_t>(y0) * mapWidth + x0;
const size_t i10 = static_cast<size_t>(y0) * mapWidth + x1;
const size_t i01 = static_cast<size_t>(y1) * mapWidth + x0;
const size_t i11 = static_cast<size_t>(y1) * mapWidth + x1;
const double xTop = static_cast<double>(mapX[i00]) * (1.0 - tx) +
static_cast<double>(mapX[i10]) * tx;
const double xBottom = static_cast<double>(mapX[i01]) * (1.0 - tx) +
static_cast<double>(mapX[i11]) * tx;
const double yTop = static_cast<double>(mapY[i00]) * (1.0 - tx) +
static_cast<double>(mapY[i10]) * tx;
const double yBottom = static_cast<double>(mapY[i01]) * (1.0 - tx) +
static_cast<double>(mapY[i11]) * tx;
rawX = xTop * (1.0 - ty) + xBottom * ty;
rawY = yTop * (1.0 - ty) + yBottom * ty;
return std::isfinite(rawX) && std::isfinite(rawY);
}
void appendUniqueDir(QStringList& dirs, const QString& dir)
{
if (dir.isEmpty())
@ -325,18 +388,6 @@ bool validateInput(const DroneScrewInputImage& leftImage,
return true;
}
void appendRoiBox(const StereoBoltModuleRoiC& roi, DroneScrewResult& result)
{
DroneScrewBox box;
box.classId = roi.class_id;
box.score = static_cast<float>(roi.score);
box.x = roi.x;
box.y = roi.y;
box.width = roi.width;
box.height = roi.height;
result.boxes.push_back(box);
}
void appendDistance(const StereoBoltModuleDistanceC& distance, DroneScrewResult& result)
{
DroneScrewDistance d;
@ -452,6 +503,203 @@ DroneScrewAlgoStub::~DroneScrewAlgoStub()
UnInit();
}
bool DroneScrewAlgoStub::buildRectifiedLeftToRawMap(const StereoBoltCalibC& calib,
double rectAlpha,
std::string& error)
{
m_rectMapWidth = 0;
m_rectMapHeight = 0;
m_leftRectToRawX.clear();
m_leftRectToRawY.clear();
if (calib.image_width <= 0 || calib.image_height <= 0)
{
error = "invalid calibration image size for rectified-to-raw map";
return false;
}
if (!std::isfinite(rectAlpha))
{
error = "invalid rectification alpha for rectified-to-raw map";
return false;
}
try
{
const cv::Size imageSize(calib.image_width, calib.image_height);
const cv::Mat K1 = mat64(3, 3, calib.left_K);
const cv::Mat D1 = mat64(1, 5, calib.left_D);
const cv::Mat K2 = mat64(3, 3, calib.right_K);
const cv::Mat D2 = mat64(1, 5, calib.right_D);
const cv::Mat R = mat64(3, 3, calib.R);
const cv::Mat T = mat64(3, 1, calib.T);
cv::Mat R1;
cv::Mat R2;
cv::Mat P1;
cv::Mat P2;
cv::Mat Q;
cv::Rect validRoi1;
cv::Rect validRoi2;
cv::stereoRectify(K1, D1, K2, D2, imageSize, R, T,
R1, R2, P1, P2, Q,
cv::CALIB_ZERO_DISPARITY,
rectAlpha,
imageSize,
&validRoi1,
&validRoi2);
cv::Mat mapX;
cv::Mat mapY;
cv::initUndistortRectifyMap(K1, D1, R1, P1, imageSize,
CV_32FC1, mapX, mapY);
if (mapX.empty() || mapY.empty() ||
mapX.cols != imageSize.width || mapX.rows != imageSize.height ||
mapY.cols != imageSize.width || mapY.rows != imageSize.height)
{
error = "OpenCV returned invalid rectified-to-raw map";
return false;
}
const size_t total = static_cast<size_t>(imageSize.width) *
static_cast<size_t>(imageSize.height);
m_leftRectToRawX.resize(total);
m_leftRectToRawY.resize(total);
for (int y = 0; y < imageSize.height; ++y)
{
const float* srcX = mapX.ptr<float>(y);
const float* srcY = mapY.ptr<float>(y);
const size_t offset = static_cast<size_t>(y) * imageSize.width;
std::copy(srcX, srcX + imageSize.width, m_leftRectToRawX.begin() + offset);
std::copy(srcY, srcY + imageSize.width, m_leftRectToRawY.begin() + offset);
}
m_calib = calib;
m_rectAlpha = rectAlpha;
m_rectMapWidth = imageSize.width;
m_rectMapHeight = imageSize.height;
return true;
}
catch (const cv::Exception& e)
{
error = std::string("OpenCV rectified-to-raw map failed: ") + e.what();
}
catch (const std::exception& e)
{
error = std::string("rectified-to-raw map failed: ") + e.what();
}
catch (...)
{
error = "rectified-to-raw map failed: unknown exception";
}
m_rectMapWidth = 0;
m_rectMapHeight = 0;
m_leftRectToRawX.clear();
m_leftRectToRawY.clear();
return false;
}
bool DroneScrewAlgoStub::mapRectifiedLeftRoi(const StereoBoltModuleRoiC& roi,
int rawWidth,
int rawHeight,
DroneScrewBox& box) const
{
if (roi.width <= 0 || roi.height <= 0)
return false;
if (m_rectMapWidth <= 0 || m_rectMapHeight <= 0 ||
m_leftRectToRawX.empty() || m_leftRectToRawY.empty())
{
return false;
}
const int dstWidth = rawWidth > 0 ? rawWidth : m_rectMapWidth;
const int dstHeight = rawHeight > 0 ? rawHeight : m_rectMapHeight;
if (dstWidth <= 0 || dstHeight <= 0)
return false;
const double rectLeft = static_cast<double>(roi.x);
const double rectTop = static_cast<double>(roi.y);
const double rectRight = static_cast<double>(roi.x) + static_cast<double>(roi.width) - 1.0;
const double rectBottom = static_cast<double>(roi.y) + static_cast<double>(roi.height) - 1.0;
if (rectRight < 0.0 || rectBottom < 0.0 ||
rectLeft > static_cast<double>(m_rectMapWidth - 1) ||
rectTop > static_cast<double>(m_rectMapHeight - 1))
{
return false;
}
double minRawX = std::numeric_limits<double>::infinity();
double minRawY = std::numeric_limits<double>::infinity();
double maxRawX = -std::numeric_limits<double>::infinity();
double maxRawY = -std::numeric_limits<double>::infinity();
bool hasSample = false;
const int samplesX = roiSampleCount(rectRight - rectLeft);
const int samplesY = roiSampleCount(rectBottom - rectTop);
for (int sy = 0; sy < samplesY; ++sy)
{
const double ty = (samplesY <= 1) ? 0.0 : static_cast<double>(sy) / (samplesY - 1);
const double rectY = rectTop + (rectBottom - rectTop) * ty;
for (int sx = 0; sx < samplesX; ++sx)
{
const double tx = (samplesX <= 1) ? 0.0 : static_cast<double>(sx) / (samplesX - 1);
const double rectX = rectLeft + (rectRight - rectLeft) * tx;
double rawX = 0.0;
double rawY = 0.0;
if (!sampleMapBilinear(m_leftRectToRawX, m_leftRectToRawY,
m_rectMapWidth, m_rectMapHeight,
rectX, rectY, rawX, rawY))
{
continue;
}
minRawX = std::min(minRawX, rawX);
minRawY = std::min(minRawY, rawY);
maxRawX = std::max(maxRawX, rawX);
maxRawY = std::max(maxRawY, rawY);
hasSample = true;
}
}
if (!hasSample)
return false;
if (maxRawX < 0.0 || maxRawY < 0.0 ||
minRawX > static_cast<double>(dstWidth - 1) ||
minRawY > static_cast<double>(dstHeight - 1))
{
return false;
}
const int left = std::max(0, std::min(dstWidth - 1,
static_cast<int>(std::floor(minRawX))));
const int top = std::max(0, std::min(dstHeight - 1,
static_cast<int>(std::floor(minRawY))));
const int right = std::max(0, std::min(dstWidth - 1,
static_cast<int>(std::ceil(maxRawX))));
const int bottom = std::max(0, std::min(dstHeight - 1,
static_cast<int>(std::ceil(maxRawY))));
if (right < left || bottom < top)
return false;
box.classId = roi.class_id;
box.score = static_cast<float>(roi.score);
box.x = left;
box.y = top;
box.width = right - left + 1;
box.height = bottom - top + 1;
return true;
}
void DroneScrewAlgoStub::appendRoiBox(const StereoBoltModuleRoiC& roi,
DroneScrewResult& result) const
{
DroneScrewBox box;
if (mapRectifiedLeftRoi(roi, result.imageWidth, result.imageHeight, box))
result.boxes.push_back(box);
}
int DroneScrewAlgoStub::Init(const DroneScrewAlgoParams& params)
{
std::lock_guard<std::mutex> lk(m_mutex);
@ -464,6 +712,10 @@ int DroneScrewAlgoStub::Init(const DroneScrewAlgoParams& params)
sb_destroy(m_ctx);
m_ctx = nullptr;
}
m_rectMapWidth = 0;
m_rectMapHeight = 0;
m_leftRectToRawX.clear();
m_leftRectToRawY.clear();
QStringList configBases;
appendUniqueDir(configBases, QDir::currentPath());
@ -536,10 +788,22 @@ int DroneScrewAlgoStub::Init(const DroneScrewAlgoParams& params)
algoParams.rng_lrc_trusted_mm =
yamlDouble(yaml, "ranging.lrc_trusted_mm", algoParams.rng_lrc_trusted_mm);
if (!buildRectifiedLeftToRawMap(calib, algoParams.rect_alpha, loadError))
{
m_initError = std::string("stereo_bolt build bbox rectified-to-raw map failed calib=") +
calibPath.toStdString() + " err=" + loadError;
LOG_ERROR("[ALGO] %s\n", m_initError.c_str());
return ERR_CODE(DEV_OPEN_ERR);
}
m_ctx = sb_create_ex(&calib, &model, &algoParams);
if (!m_ctx)
{
m_rectMapWidth = 0;
m_rectMapHeight = 0;
m_leftRectToRawX.clear();
m_leftRectToRawY.clear();
const char* err = sb_last_error(nullptr);
m_initError = std::string("stereo_bolt create_ex failed config=") +
configPath.toStdString() +
@ -578,6 +842,10 @@ int DroneScrewAlgoStub::UnInit()
sb_destroy(m_ctx);
m_ctx = nullptr;
}
m_rectMapWidth = 0;
m_rectMapHeight = 0;
m_leftRectToRawX.clear();
m_leftRectToRawY.clear();
return 0;
}

View File

@ -37,12 +37,32 @@ public:
std::string GetVersion() const override;
private:
#ifdef DRONESCREW_STEREO_BOLT_DIRECT_LINK
bool buildRectifiedLeftToRawMap(const StereoBoltCalibC& calib,
double rectAlpha,
std::string& error);
bool mapRectifiedLeftRoi(const StereoBoltModuleRoiC& roi,
int rawWidth,
int rawHeight,
DroneScrewBox& box) const;
void appendRoiBox(const StereoBoltModuleRoiC& roi,
DroneScrewResult& result) const;
#endif
DroneScrewAlgoParams m_params;
StereoBoltCtx* m_ctx{nullptr};
std::string m_configPath;
std::string m_calibPath;
std::string m_modelPath;
std::string m_initError;
#ifdef DRONESCREW_STEREO_BOLT_DIRECT_LINK
StereoBoltCalibC m_calib{};
double m_rectAlpha{1.0};
int m_rectMapWidth{0};
int m_rectMapHeight{0};
std::vector<float> m_leftRectToRawX;
std::vector<float> m_leftRectToRawY;
#endif
mutable std::mutex m_mutex;
std::atomic<bool> m_bInited{false};
};

View File

@ -103,7 +103,7 @@ win32:CONFIG(debug, debug|release) {
OPENCV450_LIB_DIR = $$PWD/../../../SDK/OpenCV450/Arm/aarch64/lib
INCLUDEPATH += $$PWD/../../../SDK/OpenCV450/Arm/aarch64/include/opencv4
LIBS += -L$${OPENCV450_LIB_DIR}
LIBS += -lopencv_imgcodecs -lopencv_imgproc -lopencv_core
LIBS += -lopencv_imgcodecs -lopencv_imgproc -lopencv_calib3d -lopencv_core
QMAKE_LFLAGS += -Wl,-rpath-link,$${OPENCV450_LIB_DIR}
# MVS SDK

View File

@ -67,7 +67,9 @@ constexpr int64_t kDistanceDecimationVertical = 4;
constexpr int kDetectPipelinePrecision = 0;
constexpr int kDetectPipelineDistance = 1;
constexpr const char* kImageSaveRootDir = "/home/cat";
constexpr size_t kMaxImageSaveQueueDepth = 16;
constexpr size_t kMaxImageSaveQueueDepth = 100;
constexpr size_t kImageSaveWorkerCount = 4;
constexpr int kDistanceImageSaveStride = 3;
const char* mvsSdkErrorName(int code)
{
@ -1812,9 +1814,9 @@ int DroneScrewServerPresenter::stopDetectionWork()
stopGpioTriggerLoop();
m_bThreadExit = true;
stopImageSaveThread();
if (m_detectThread.joinable())
m_detectThread.join();
stopImageSaveThread();
m_bIsDetecting = false;
m_detectPipelineMode = kDetectPipelinePrecision;
m_activeTriggerFps = static_cast<int>(kPrecisionFrameRate);
@ -1933,9 +1935,9 @@ int DroneScrewServerPresenter::stopLiveStream()
m_bRtspStarted = false;
stopGpioTriggerLoop();
m_bThreadExit = true;
stopImageSaveThread();
if (m_detectThread.joinable())
m_detectThread.join();
stopImageSaveThread();
m_bIsDetecting = false;
m_detectPipelineMode = kDetectPipelinePrecision;
m_activeTriggerFps = static_cast<int>(kPrecisionFrameRate);
@ -2356,28 +2358,51 @@ void DroneScrewServerPresenter::startImageSaveThread(const QString& modeName)
m_imageSaveThreadExit = false;
}
m_imageSaveThread = std::thread(&DroneScrewServerPresenter::imageSaveThreadFunc, this);
LOG_INFO("[SAVE] image save thread started mode=%s dir=%s\n",
modeName.toStdString().c_str(), sessionDir.toStdString().c_str());
m_imageSaveThreads.reserve(kImageSaveWorkerCount);
for (size_t i = 0; i < kImageSaveWorkerCount; ++i)
{
m_imageSaveThreads.emplace_back(&DroneScrewServerPresenter::imageSaveThreadFunc,
this,
static_cast<int>(i));
}
LOG_INFO("[SAVE] image save workers started count=%zu mode=%s dir=%s\n",
m_imageSaveThreads.size(),
modeName.toStdString().c_str(),
sessionDir.toStdString().c_str());
}
void DroneScrewServerPresenter::stopImageSaveThread()
{
const bool shouldJoin = m_imageSaveThread.joinable();
const bool shouldJoin = !m_imageSaveThreads.empty();
size_t droppedPending = 0;
{
std::lock_guard<std::mutex> lk(m_imageSaveMutex);
m_imageSaveQueue.clear();
if (!shouldJoin)
{
m_imageSaveQueue.clear();
m_imageSaveSessionDir.clear();
m_imageSaveThreadExit = false;
return;
}
droppedPending = m_imageSaveQueue.size();
if (droppedPending > 0)
m_imageSaveQueue.clear();
m_imageSaveThreadExit = true;
}
if (droppedPending > 0)
{
LOG_WARN("[SAVE] stop requested, drop pending save jobs=%zu\n",
droppedPending);
}
m_imageSaveCv.notify_all();
m_imageSaveThread.join();
for (std::thread& worker : m_imageSaveThreads)
{
if (worker.joinable())
worker.join();
}
m_imageSaveThreads.clear();
{
std::lock_guard<std::mutex> lk(m_imageSaveMutex);
@ -2457,9 +2482,9 @@ void DroneScrewServerPresenter::enqueueImageSave(const MvsImageData& leftImg,
m_imageSaveCv.notify_one();
}
void DroneScrewServerPresenter::imageSaveThreadFunc()
void DroneScrewServerPresenter::imageSaveThreadFunc(int workerIndex)
{
LOG_DEBUG("[SAVE] worker entered\n");
LOG_DEBUG("[SAVE] worker #%d entered\n", workerIndex);
for (;;)
{
ImageSaveJob job;
@ -2470,10 +2495,15 @@ void DroneScrewServerPresenter::imageSaveThreadFunc()
});
if (m_imageSaveThreadExit.load())
{
m_imageSaveQueue.clear();
break;
}
if (m_imageSaveQueue.empty())
{
continue;
}
job = std::move(m_imageSaveQueue.front());
m_imageSaveQueue.pop_front();
@ -2502,9 +2532,11 @@ void DroneScrewServerPresenter::imageSaveThreadFunc()
};
saveMono8(job.left, QStringLiteral("left"));
if (m_imageSaveThreadExit.load())
break;
saveMono8(job.right, QStringLiteral("right"));
}
LOG_DEBUG("[SAVE] worker stopped\n");
LOG_DEBUG("[SAVE] worker #%d stopped\n", workerIndex);
}
DroneScrewResult DroneScrewServerPresenter::runSingleDetection()
@ -2800,6 +2832,7 @@ void DroneScrewServerPresenter::detectThreadFunc()
}
int frameCnt = 0;
unsigned long long savedFrameCnt = 0;
bool haveProcessedFrame = false;
unsigned long long lastProcessedLeftFrameId = 0;
unsigned long long lastProcessedRightFrameId = 0;
@ -2875,7 +2908,12 @@ void DroneScrewServerPresenter::detectThreadFunc()
haveProcessedFrame = true;
lastProcessedLeftFrameId = leftImg.frameID;
lastProcessedRightFrameId = rightImg.frameID;
enqueueImageSave(leftImg, rightImg, static_cast<unsigned long long>(frameCnt + 1));
const bool distanceSaveMode =
(m_detectPipelineMode.load() == kDetectPipelineDistance);
const bool shouldSaveFrame =
!distanceSaveMode || ((frameCnt % kDistanceImageSaveStride) == 0);
if (shouldSaveFrame)
enqueueImageSave(leftImg, rightImg, ++savedFrameCnt);
// Publish the display frame before detection work. detectMode only controls raw transport.
if (m_bRawPubEnabled.load())

View File

@ -236,7 +236,7 @@ private:
unsigned int timeoutMs);
void startImageSaveThread(const QString& modeName);
void stopImageSaveThread();
void imageSaveThreadFunc();
void imageSaveThreadFunc(int workerIndex);
void enqueueImageSave(const MvsImageData& leftImg,
const MvsImageData& rightImg,
unsigned long long index);
@ -344,7 +344,7 @@ private:
bool m_bRightImageReady{false};
// 异步图像保存
std::thread m_imageSaveThread;
std::vector<std::thread> m_imageSaveThreads;
std::atomic<bool> m_imageSaveThreadExit{false};
std::mutex m_imageSaveMutex;
std::condition_variable m_imageSaveCv;

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 "2"
#define DRONESCREWSERVER_VERSION_BUILD "3"
#endif // DRONESCREW_VERSION_H

View File

@ -1,13 +1,38 @@
# ABI fingerprint — self-contained delivery
# OpenCV 4.5.0 (source build, standard SONAME — no Debian 'd' suffix)
#
# Target: aarch64, Ubuntu 22.04, glibc >= 2.32, RK3588 NPU driver
# Bundled: OpenCV 4.5.0 + librknnrt 2.3.2, total 9 libs in lib/
# SONAME: libopencv_core.so.4.5 (NOT .4.5d)
# System: libc / libpthread / libm / libdl / libstdc++ / libgcc_s
#
# Install:
# tar xzf stereo_bolt_delivery_selfcontained.tar.gz
# cd stereo_bolt_delivery
# export LD_LIBRARY_PATH=/e/02_Modules/StereoVisionBoltMeasurement_cpp/lib:
# ldd lib/libstereo_bolt.so | grep "not found" # should be empty
# 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)
# libstereo_bolt.so SHA256:
# 67d9406c1ab69c36b8f3b363fe6cf38567fdac97c25fa160e4c80a4d63b172ae
[arch]
AArch64
[exported symbols] (9 total: lifecycle + precise + ranging)
# --- lifecycle / construction ---
sb_create # create from a config.yaml path (reads files from disk)
sb_create_ex # NEW: create from in-memory structs (zero file IO / no path / no perms)
sb_default_params # NEW: prefilled StereoBoltParamsC for sb_create_ex
sb_destroy
sb_last_error
sb_free_bolt_module_result
# --- precise measurement (12MP) ---
sb_process_bolt_module_files # precise, file variant
sb_process_bolt_module_buffers # precise, in-memory buffer variant
# --- binned ranging (3MP) ---
sb_range_bolt_binned # binned ranging, in-memory buffer
[min glibc required] (your board's glibc must be >= this)
GLIBC_2.32
[NEEDED shared libraries] (provide matching sonames at runtime)
librknnrt.so
libopencv_calib3d.so.4.5
libopencv_imgcodecs.so.4.5
libopencv_dnn.so.4.5
libopencv_imgproc.so.4.5
libopencv_core.so.4.5
libstdc++.so.6
libm.so.6
libgcc_s.so.1
libc.so.6
ld-linux-aarch64.so.1

View File

@ -0,0 +1,67 @@
CHANGELOG — libstereo_bolt delivery package
============================================
## 2026-07-01 v3 — stable OBB-axis ranging summary
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.
Algorithm:
- Ranging now uses left/right OBB-axis pairing before the NCC fallback.
- Even-count OBB candidates no longer average two different pair hypotheses.
The summary distance chooses the center candidate with the more stable sampled
axis disparity, removing the RK3588 1498/1652 mm jump.
Performance / validation:
- RK3588, distance_153304_403, 21 binned frames:
found=21/21, distance mean=1652.360 mm, min=1652.091, max=1652.582,
std=0.149 mm, elapsed mean=153.146 ms.
- Windows ctest: 22/22 pass.
## 2026-06-30 v2 — OBB model + plane skip optimization
Changes vs previous delivery (2026-06-24, v1):
### Model
- Previous package used an HBB detector at imgsz 960.
- Current package uses best_obb_1024.rknn (OBB, imgsz 1024, YOLOv8 oriented bbox).
- OBB model trained on mixed 5MP + 12MP data (2026-06-29)
- Auto-detection: library detects OBB vs HBB from RKNN output shape
### Config
- yolo.task: "auto" (auto-detect OBB/HBB from model output)
- yolo.imgsz: 960 -> 1024
- yolo.conf: 0.5 (unchanged)
- measurement.height_from_plane: false (height = top-to-foot along axis)
- working_distance.z_min_mm: 1200 -> 900
- working_distance.z_max_mm: 2800 -> 4000
- ranging.z_min_mm: 1300 -> 900
- ranging.z_max_mm: 3600 -> 4000
### Algorithm
- Plane computation skipped when height_from_plane=false
(saves 300-550ms per frame on 8-bolt scenes)
- OBB auto-detection in RKNN backend
- OBB centerline rescue + peer height consensus clamp
### Calibration
- Unchanged: stereo_calib_20260621_202857_ascii.xml (baseline 482mm)
### C API
- ABI unchanged: c_api.h has zero changes
- All function signatures identical
- Struct layouts identical
- Drop-in .so replacement, no recompile needed for downstream
### Performance (RK3588 board, 8-bolt scene, height_from_plane=false)
- Precise: ~400ms/frame (was ~800ms with plane enabled)
- Ranging: superseded by v3 benchmark above
### Verified on
- RK3588 board (linaro): 0621 dataset 21 frames, 12/21 success
- 0627 dataset 10 frames: 10/10 success
- 0628 dataset 10 frames: 10/10 success
- Windows ctest at the time: 20/20 pass

View File

@ -1,166 +1,226 @@
# libstereo_bolt 交付说明(.so + 头文件)
你拿到的是**预编译的 aarch64 动态库 + C 头文件**,不含算法源码。
OpenCV 4.5.4、RKNN runtime 及全部传递依赖(约 131 个 `.so`**已随包内置于 `lib/`**
无需在目标板上安装任何软件包NPU 内核驱动由固件提供,不在用户态包内)。
你拿到的是**预编译的 aarch64 动态库 + C 头文件 + 模型 + 标定 + 示例**,不含算法源码。
运行时依赖中 **`libstereo_bolt.so`、OpenCV 4.5.0 `.so``librknnrt.so` 均已随包 `lib/`**;无需 apt 安装 OpenCV。NPU 驱动由板子系统提供。
> **本包标定 = 12MP 生产相机4096×3000。** 喂进来的左右图必须是该相机的 **4096×3000**
> `config``image.width=4096 / height=3000` 已配好,指向 `calib/stereo_calib_20260607_211135.xml`)。
> **包内不含测试图** —— 请用你自己这套 12MP 相机拍的左右图。
> ✅ **已在板上实测**:本 12MP 标定**加载 + rectify 通过**4096×3000stereo_rms=0.373、baseline=200.19、f_rect=3330
> ⚠️ 但**检测 + 测量在该相机上还没用真实螺杆图验过**(包内无螺杆图)。
> ⚠️ 本标定单相机 RMS=**0.373**5MP 那套是 0.19),已把 `calibration.max_single_rms` **放宽到 0.40** 才放行;
> 0.373 偏松会折损测量精度,建议后续**重标到 <0.20** 再收回此闸首次上机务必用真实螺杆图核对结果
> **本库导出两个独立的产品接口**(同一个 `.so`、同一个 `StereoBoltCtx`
> 1. **精测PRECISE** `sb_process_bolt_module_buffers` / `_files` —— 12MP 高精度测量,
> 每颗螺栓高度 / 相邻间距 / 地平面 / 3D 中心轴 / 顶底 3D 坐标。**触发式"测得准"路径(板上约 400ms/帧,依输入和 QC 耗时波动)。**
> 2. **测距RANGING** `sb_range_bolt_binned` —— 2×2 binned(3MP) 实时测距,
> 输出相机→螺栓代表直线距离。**无人机实时路径(统一用 OBB@1024,当前 distance_153304_403 复测约 153ms/帧)。**
>
> **创建上下文推荐用内存接口 `sb_create_ex`**(见下节):标定、模型、参数全部经接口/buffer 传入,
> 库**零文件 IO、无路径假设、无安装目录读权限问题**。旧的 `sb_create(config.yaml)` 仍保留作向后兼容。
---
## ★ 创建上下文:`sb_create_ex`推荐vs `sb_create`(兼容)
两个产品接口共用同一个 `StereoBoltCtx`。创建它有两种方式:
### 推荐:`sb_create_ex` —— 全参数经接口传入,零文件 IO
```c
StereoBoltCtx* sb_create_ex(const StereoBoltCalibC* calib,
const StereoBoltModelC* model,
const StereoBoltParamsC* params);
StereoBoltParamsC sb_default_params(void); /* 预填默认值,再覆盖你关心的 */
```
- `StereoBoltCalibC`:标定 —— 左右 `K[9]`/`D[5]` + `R[9]`/`T[3]` + `baseline_mm` + 图像尺寸。**替代标定 XML。**
- `StereoBoltModelC`:模型 —— `.rknn` 字节流 `data`+`size` + `backend`(rk3588 用 `SB_YOLO_BACKEND_RKNN`) + `imgsz`/`conf`/`iou`。**替代 weights 文件路径。**
- `StereoBoltParamsC`:算法数值参数 —— 视差带(`wd_z_min/max_mm` 物理带,**或** `sgbm_min/num_disparities` 固定带,二选一必填)、`rectification``measurement.foot_rim``ranging.*`。**替代 config.yaml 数值键。** 带 `struct_size` 做前向兼容。
**这条路径库不碰磁盘**:标定/模型从哪来由你决定(你的标定流程、资源文件、相机 SDK本包提供的 `calib/*.xml``weights/*.rknn` 只是**给你读进来填结构体**的现成数据。
### 兼容:`sb_create(const char* config_yaml_path)`
`config.rk3588.12mp.yaml`(其内部再按相对路径找标定 XML + 模型)。**有两文件依赖 + 工作目录/读权限要求**——正是 `sb_create_ex` 要规避的。仅在离线/快速测试时用。
> `sb_create` / `sb_create_ex` **只调一次**(载配置+标定+模型进 NPU之后每帧复用同一个 `ctx`
> `sb_destroy` / `sb_last_error` 两接口共用。线程安全:同一 `ctx` 别多线程并发。
---
## 交付边界(职责划分)
**本交付物是一个"算法模块",不是完整软件系统。** 软件总体集成由你(接入方)负责。
| 项 | 由谁负责 |
|---|---|
| 双目螺栓**测量/测距算法**`.so` + `.h` + 模型 + 标定 + 示例 + 文档) | **算法方(本包提供)** |
| 板子**运行时环境**aarch64 + glibc、NPU 驱动;算法依赖的 OpenCV 与 `librknnrt.so` 已随包 `lib/` | **接入方** |
| **取图**(相机驱动 / Mono8 buffer 喂入、上层业务、UI、调度、与上位机/无人机通信 | **接入方** |
| **标定数据来源**(填入 `StereoBoltCalibC`)、按 `c_api.h` 调用、对 height/distance/confidence 做业务决策 | **接入方** |
> 算法方只交付"喂进左右图 → 返回螺栓高度/距离"的纯算法能力C ABI。本库不碰相机、不碰网络、
> **不碰磁盘(`sb_create_ex` + buffer 接口)**、不做悬停/对中等业务决策。逐颗 `confidence`(trusted/suspect) + `status` 由接入方取舍。
## 包内容
| 路径 | 说明 |
|---|---|
| `lib/libstereo_bolt.so` | 预编译共享库aarch64 |
| `lib/librknnrt.so` 等(共 ~131 个) | OpenCV 4.5.4 + 全部传递依赖(**已随包,无需 apt** |
| `lib/libstereo_bolt.so` | 预编译共享库aarch64`sb_create_ex` 内存接口) |
| `include/stereo_bolt/c_api.h` | C ABI 头文件(唯一对外接口) |
| `weights/best_1280_20260621.rknn` | NPU 模型(精测 + 测距共用imgsz 1280 |
| `calib/stereo_calib_20260621_202857_ascii.xml` | 出厂标定0621 装配baseline 482 mm |
| `config/config.rk3588.12mp.yaml` | 配置文件(兼容 `sb_create` 接口用) |
| `example/` | 最小 C 示例(精测 + 测距) + CMakeLists |
| `INSTALL_3588.md` | 安装与运行说明(**从这里开始** |
| `DEPENDENCIES.md` | 依赖库详细清单及说明 |
| `ABI_FINGERPRINT.txt` | 二进制 ABI 指纹 |
| `weights/best_obb_1024.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` 加载 |
| `INSTALL_3588.md` / `ABI_FINGERPRINT.txt` | 板上装环境说明 / .so 二进制指纹(**你的环境必须匹配** |
| `example/` | C 调用示例(`main.c` 精测,**sb_create + buffer 接口**+ CMakeLists |
---
## 你什么时候需要什么(编译期 vs 运行期)
## 编译期 vs 运行期
**重点:你不需要重编这个库**(它是预编译好的成品,你也没有源码)。你只编译*你自己*
调用它的小程序那一步几乎零依赖。OpenCV / librknnrt 是**运行那一刻**才需要的。
**你不需要重编这个库**(预编译成品,无源码)。你只编译*你自己*调用它的程序。
| 东西 | 编译你自己的调用程序 | 在板上**运行** |
| 东西 | 编译你的调用程序 | 板上运行 |
|---|:---:|:---:|
| `c_api.h` | ✅ | — |
| `libstereo_bolt.so` | ✅(链接,带 `--allow-shlib-undefined` | ✅ |
| OpenCV 4.5.4 | ❌ 不需要C ABI 不暴露 cv 类型) | ✅ **已随包 `lib/`,无需安装** |
| `librknnrt.so`RKNN 运行时) | ❌ | ✅ **已随包 `lib/`** |
| OpenCV | 你的程序:❌ 不需要C ABI 不暴露 cv 类型,相机直接给 Mono8 buffer<br>本包 example❌ 运行不需要系统 OpenCV | ✅ 已随包 `lib/`,无需 apt |
| `librknnrt.so` | ❌ | ✅ 已随包 `lib/``LD_LIBRARY_PATH=./lib` |
| NPU 驱动 + `/dev/rknpu` | ❌ | ✅ 必须(内核态) |
| `.rknn` 模型 + 标定 XML + `config` | ❌ | ✅ 必须(**已在本包** weights/ config/ calib/ |
| 模型 / 标定 | ❌ | ✅(已在本包;`sb_create_ex` 由你读进来传,`sb_create` 由库读 |
> **“YOLO 依赖”在板上很轻**:就是 `librknnrt.so` + NPU 驱动 + `.rknn` 文件。
> **不需要** Python / PyTorch / ultralytics / ONNXRuntime / rknn-toolkit2 —— 那些是 PC 上把
> `.onnx` 转成 `.rknn` 用的,转完板上就用不到。
> example 用 OpenCV 只是为了独立跑(读本包的标定 XML + 测试图)。**你的产品里相机直接给 Mono8 buffer、
> 标定来自你自己的来源,调用端无需 OpenCV库也始终零文件 IO。**
---
## ⚠️ 必须匹配的 ABI 指纹(动态链接的硬约束)
## ⚠️ 必须匹配的 ABI 指纹
这个 `.so` 在某个具体环境里编出来,二进制里**写死了**它依赖的 so 名字和 glibc 版本。
**“自己准备 OpenCV” 不等于随便哪个 OpenCV 都行——必须是指纹里那个 major.minor。**
`.so` 二进制里写死了依赖的 so 名字和 glibc 版本。打开 `ABI_FINGERPRINT.txt` 确保板子满足:
- CPU = **aarch64****glibc ≥ 指纹版本**GLIBC_2.32
- **运行时优先使用本包 `lib/`**(内含标准 SONAME `libopencv_*.so.4.5`,无需系统 OpenCV
- **librknnrt 版本** = 转 `.rknn` 的 toolkit 版本(**2.3.x**)。
- 指纹应列出 9 个导出符号,含 **`sb_create_ex` / `sb_default_params`**。
打开 `ABI_FINGERPRINT.txt`,确保你的板子满足:
- CPU 架构 = **aarch64**
- **glibc ≥ 指纹里的版本**(低于它,加载即 `GLIBC_x.xx not found`
- **OpenCV soname 与指纹一致**(本包指纹 = `libopencv_core.so.4.5d` ⇒ 装 **OpenCV 4.5.x**;其它版本 soname 不通用)
- **librknnrt 版本** = 转换 `.rknn` 所用 toolkit 版本(**2.3.x**
一条命令自检(应当**没有任何** `not found`
```bash
ldd lib/libstereo_bolt.so | grep "not found"
```
自检(应无任何 `not found``ldd lib/libstereo_bolt.so | grep "not found"`
---
## 板子上需要准备的
## 公共错误码
| 依赖 | 状态 |
|---|---|
| OpenCV 4.5.4 + 传递依赖131 个 .so | ✅ **已随包 `lib/`**,无需 apt |
| `librknnrt.so`RKNN 运行时 2.3.2 | ✅ **已随包 `lib/`** |
| `.rknn` 模型 + 标定 XML + config | ✅ **已随包** `weights/` `calib/` `config/` |
| NPU 内核驱动 + `/dev/rknpu` | ⚠️ **板子固件提供**,出厂 RK3588 一般已有 |
| glibc ≥ 2.32Ubuntu 22.04 自带 2.35 | ✅ 系统自带,无需操作 |
> 完整依赖说明见 `DEPENDENCIES.md`
`SB_OK=0` / `SB_ERR_INVALID_CONFIG=-1` / `SB_ERR_IO=-2` / `SB_ERR_ALGORITHM_REJECTED=-3` / `SB_ERR_INTERNAL=-99`
> `SB_ERR_ALGORITHM_REJECTED` 不是崩溃,是算法判定该帧不满足 `expected` 颗(仅精测返回);**仍返回已测出的部分螺栓**(见下)。
---
## 怎么用C ABI
## 接口 1 —— 精测PRECISE
完整示例见 `example/main.c`。有**两个**喂帧入口,生产用内存版:
**用途**12MP 逐颗高精度测量。触发式"测得准"路径,板上 ~2.5Hz@1024
**输入**:内存 Mono8 buffer生产首选或文件路径离线。buffer 尺寸**必须等于标定尺寸**,否则 `SB_ERR_IO`
**① 生产路径 —— 内存版(不落盘,直接喂相机帧):**
```c
StereoBoltCtx* ctx = sb_create("config/config.rk3588.yaml"); // 一次:载配置/标定/RKNN 模型
while (抓帧) {
const uint8_t* L = camera_left_frame(); // 相机 SDK 给的 Mono8 缓冲(W*H 字节,在内存)
const uint8_t* R = camera_right_frame();
StereoBoltModuleResultC out;
sb_process_bolt_module_buffers(ctx,
L, W, H, 0, // stride=0 表示行紧凑排列(每行 W 字节)
R, W, H, 0,
1, // channels: 1=Mono8(灰度), 3=BGR 也接受(内部转灰度)
8, &out); // 8 = 期望螺栓数
/* out.success==1: out.data.bolts[i].height_mm / foot_xyz / top_xyz + adjacent_distances + ground_plane
out.success==0: out.failure.reason + left_rois/right_rois/pairs(诊断用) */
sb_free_bolt_module_result(&out);
}
sb_destroy(ctx); // 一次
sb_status_t sb_process_bolt_module_buffers(
StereoBoltCtx* ctx,
const uint8_t* left_data, int left_width, int left_height, int left_stride_bytes,
const uint8_t* right_data, int right_width, int right_height, int right_stride_bytes,
int channels, int expected_bolt_count, StereoBoltModuleResultC* out);
sb_status_t sb_process_bolt_module_files(StereoBoltCtx*, const char* left, const char* right,
int expected_bolt_count, StereoBoltModuleResultC* out);
void sb_free_bolt_module_result(StereoBoltModuleResultC* out); /* 用完必须释放 */
```
> `W`/`H` **必须等于标定分辨率**(`config``image.width`/`image.height`),否则返回 `SB_ERR_IO`
> 内存版**全程不碰磁盘**(已实测:删掉源文件后内存版照常出结果)。
**② 离线/测试路径 —— 文件版(读磁盘图,用你自己的 12MP 左右图):**
**输出 `StereoBoltModuleResultC`** —— 每颗 `data.bolts[i]`
- **`height_mm`**(高度)
- **`top_xyz` / `foot_xyz`**(顶 / 底 3D 坐标)
- **`centerline`**3D 中心轴:`point` + `direction`
- `left_roi` / `right_roi`(左右图 2D 框 x,y,w,h,score
- **`confidence`**0=trusted / 1=suspect+ `status`
另含 `adjacent_distances[]`(相邻 `distance_mm`)、`ground_plane``model[4]`/`rms_mm`/`inlier_count`)。
**坐标系/单位**高度、间距、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` 自行取舍。**
**最简用法(`sb_create_ex`**
```c
sb_process_bolt_module_files(ctx, "your_left.bmp", "your_right.bmp", 8, &out);
StereoBoltCalibC calib = {...}; /* 填 K/D/R/T/size/baseline */
StereoBoltModelC model = {.data=rknn_bytes, .size=n, .backend=SB_YOLO_BACKEND_RKNN, .imgsz=1024, .conf=0.5f};
StereoBoltParamsC p = sb_default_params(); p.wd_z_min_mm=1200; p.wd_z_max_mm=2800;
StereoBoltCtx* ctx = sb_create_ex(&calib, &model, &p); /* 一次 */
StereoBoltModuleResultC out;
sb_process_bolt_module_buffers(ctx, L,w,h,0, R,w,h,0, 1, /*expected=*/6, &out);
/* 遍历 out.data.bolts[i].height_mm / top_xyz / foot_xyz ... */
sb_free_bolt_module_result(&out); sb_destroy(ctx);
```
> 图必须是本套 12MP 相机的 4096×3000(灰度或彩色均可),格式 .bmp/.tiff/.png。本包不含测试图。
错误码:`SB_OK=0` / `SB_ERR_INVALID_CONFIG=-1` / `SB_ERR_IO=-2` /
`SB_ERR_ALGORITHM_REJECTED=-3` / `SB_ERR_INTERNAL=-99`
> `SB_ERR_ALGORITHM_REJECTED` 不是崩溃,是算法判定该帧不可测,`out.failure` 里有原因 + ROI + 配对状态。
完整示例见 `example/main.c`。**imgsz 首选 1024**OBB 模型,自动检测旋转框);交付权重 FP16INT8 检出全废,不可用)。
---
## 输入 / 输出(契约)
## 高度测量模式(`measurement.height_from_plane`
**输入**
- 一次性(`sb_create``config/config.rk3588.yaml`(内部指向 `calib/*.xml``weights/best.rknn`)。
- 每帧(生产用 `sb_process_bolt_module_buffers`):左/右 Mono8 内存缓冲 + 宽/高/stride、通道数、期望螺栓数(int)、`&out`
- 每帧(离线用 `sb_process_bolt_module_files`):左图路径、右图路径、期望螺栓数(int)、`&out`
精测接口支持两种高度计算方式,通过 config 中 `measurement.height_from_plane` 切换:
**输出**`StereoBoltModuleResultC out` + 返回码)
- 成功 `out.success==1``out.data`
- `bolts[]` 每颗:`bolt_id` / `left_roi`,`right_roi`(x,y,width,height,score) / **`height_mm`** /
**`foot_xyz`,`top_xyz`**(底/顶 3D 点) / `centerline`(3D 轴 = point+direction)
- `adjacent_distances[]``bolt_id_i`,`bolt_id_j`,**`distance_mm`**
- `ground_plane``model[4]`(ax+by+cz+d=0)、`rms_mm``inlier_count`
- 失败 `out.success==0``out.failure``reason` + `left_rois[]`/`right_rois[]` + `pairs[]`(配对诊断)
| 模式 | config 值 | 高度定义 | 是否需要平面拟合 | 板上耗时8颗场景 |
|---|---|---|---|---|
| **顶到底**(默认) | `height_from_plane: false` | 螺杆顶端到可见底端沿拟合轴的长度 | **不需要**(自动跳过) | ~400ms |
| **顶到平面** | `height_from_plane: true` | 螺杆顶端到支撑平面的法向距离 | 需要StereoBM 拟合) | ~800ms |
**单位 / 坐标系**:高度、间距 = **mm**3D 坐标(foot/top/centerline) = **mm校正后左相机坐标系**
x 右 / y 下 / z=深度,典型 z≈16002200mm`score`=YOLO 置信度。
**默认关闭平面测量**`height_from_plane: false`),此时平面计算完全跳过,节省 300-550ms。
输出格式示例(**仅示意字段结构**,数值来自早先 5MP 验证机,非本包 12MP 相机):
```
success=1 bolts=8 distances=8
bolt 0 height=151.2mm top=(-191.9,-122.9,1995.3) foot=(-204.3,2.9,2078.3)
...
dist 0-1=268.3mm dist 1-2=254.4mm ... dist 7-0=335.8mm
**开启平面测量**:在 `config.rk3588.12mp.yaml` 中修改:
```yaml
measurement:
height_from_plane: true # 开启:高度 = 螺杆顶端到支撑平面的法向距离
```
> 一句话:**入 = 一对左右图 + 期望螺栓数(配置/标定/模型初始化时给一次);
> 出 = 每颗螺栓 高度 + 顶/底 3D + 3D 中心轴 + 相邻间距 + 地平面,或失败原因 + 诊断。**
`sb_create_ex` 用户:当前 `StereoBoltParamsC` 不含此字段,默认为 false顶到底。如需平面模式使用 `sb_create(config.yaml)` 并在 config 中设置。
---
## 编译你自己的调用程序 + 运行(**都在包根目录执行**
## 接口 2 —— 测距RANGING`sb_range_bolt_binned`
**用途**:实时回答"左目光心→螺栓多远"。容忍部分可见:**绝不要求螺栓数**≥1 颗即给距离。当前板上约 153ms/帧(~6.5Hz@1024
**输入**2×2 binned 内存 buffer12MP→3MP=2048×1500Mono8/BGR。binned 内参**内部推导**,无需单独 binned 标定。
```c
typedef struct { double distance_mm; int left_x,left_y,right_x,right_y; double ncc_score,lrc_px; int confidence; } StereoBoltRangeBoltC;
typedef struct { int found; double distance_mm; int bolt_count; char status[32]; } StereoBoltRangeSummaryC;
sb_status_t sb_range_bolt_binned(
StereoBoltCtx* ctx,
const uint8_t* left_data, int left_width, int left_height, int left_stride_bytes,
const uint8_t* right_data, int right_width, int right_height, int right_stride_bytes,
int channels, StereoBoltRangeSummaryC* out, StereoBoltRangeBoltC* bolts, int max_bolts);
```
**输出**:主用 `out.distance_mm`(可见杆代表距离);`out.found` 每帧轮询;`bolts[]` 给出每颗候选的左右匹配点。OBB 轴路线下 `ncc_score` 为检测/匹配分数、`lrc_px` 为轴视差自洽度(字段名保留用于 ABI 兼容NCC 回退路线下为 NCC/LRC 指标。精度目标 **±50mm**(稳定区 1.32.4m)。
**可选 `ranging.*` 参数**`StereoBoltParamsC``rng_*`,都有默认):`z_min/max_mm` 视差带、`lrc_max_mm`/`lrc_trusted_mm` 物理同根门 等。完整示例见 `example/range_example.cpp`
> 注:`sb_create_ex` 即使**只做测距**也要求提供精测视差带(`wd_z_*``sgbm_*`),因为同一 `ctx` 两接口共用。
---
## 该用哪个 + 板上实测吞吐
| | 精测 `sb_process_bolt_module_*` | 测距 `sb_range_bolt_binned` |
|---|---|---|
| 问题 | 每颗螺栓高度/间距/地平面/3D 坐标 | 相机离螺栓多远(代表距离) |
| 输入 | 12MP 原图Mono8 buffer / 文件) | 2×2 binned 3MP buffer |
| 螺栓数 | 要 `expected`(不符=部分输出+`count_mismatch` | **不要**≥1 颗即 `found` |
| 板上吞吐@1024 | **约 400ms/帧** | **约 153ms/帧 ≈ 6.5Hz** |
| 定位 | 触发式"测得准" | 无人机实时定位反馈 |
> 测距实测于 RK3588best_obb_1024.rknndistance_153304_40321 帧):均值 153.146ms,距离均值 1652.360mm,跨度 0.491mm。
> 本交付为单模型 OBB@1024(精测+测距共用)。
---
## 编译你的调用程序 + 运行
示例用 C/C++,运行时库从本包 `lib/` 加载:
```bash
# 在包根目录(与 lib/ config/ calib/ 同级)编译
cmake -S example -B example/build -DSB_ROOT="$(pwd)"
cmake --build example/build
cd example
cmake -S . -B build -DSB_ROOT="$(cd .. && pwd)"
cmake --build build # -> bolt_demo精测+ range_demo测距
# 在包根目录运行(config 里 calib/、weights/ 是相对路径,按当前目录解析)
# example 走【内存接口】: 读 raw Mono8 裸数据(W*H 字节)喂 sb_process_bolt_module_buffers,
# 库全程不见文件路径。your_left/right.raw = 你 12MP 相机帧 dump 的 Mono8(4096*3000 字节)
LD_LIBRARY_PATH=lib:/usr/lib:$LD_LIBRARY_PATH \
./example/build/bolt_demo config/config.rk3588.yaml your_left.raw your_right.raw 4096 3000 8
# ../lib 含 libstereo_bolt.so + OpenCV 4.5.0 + librknnrt.so
# 精测:传标定 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
# 测距:传标定 + 模型 + 左右图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
```

View File

@ -5,74 +5,25 @@
---
## 一、包含哪些库lib/ 目录)
## 一、包含哪些库lib/ 目录,共 9 个
### 核心算法库(本包提供)
| 库 | 说明 |
|---|---|
| `libstereo_bolt.so` | 本算法库(主体) |
| `librknnrt.so` | RKNN NPU 推理运行时Rockchip 2.3.2 |
| `libopencv_core.so.4.5` | OpenCV 4.5.0 — 基础矩阵/图像容器 |
| `libopencv_imgproc.so.4.5` | OpenCV 4.5.0 — 图像处理 |
| `libopencv_calib3d.so.4.5` | OpenCV 4.5.0 — 立体标定、rectify、三角测量 |
| `libopencv_imgcodecs.so.4.5` | OpenCV 4.5.0 — 图像文件读写 |
| `libopencv_dnn.so.4.5` | OpenCV 4.5.0 — DNN 模块 |
| `libopencv_features2d.so.4.5` | OpenCV 4.5.0 — 特征点calib3d 依赖) |
| `libopencv_flann.so.4.5` | OpenCV 4.5.0 — FLANN 索引features2d 依赖) |
### OpenCV 4.5.4Ubuntu 22.04 releaseaarch64
| 库 | 用途 |
|---|---|
| `libopencv_core.so.4.5d` | 基础矩阵/图像容器 |
| `libopencv_imgproc.so.4.5d` | 图像处理(滤波、边缘、几何变换) |
| `libopencv_calib3d.so.4.5d` | 立体标定、rectify、三角测量 |
| `libopencv_imgcodecs.so.4.5d` | 图像文件读写bmp/png/tiff |
| `libopencv_dnn.so.4.5d` | DNN 模块YOLO ONNX 推理备用后端) |
| `libopencv_features2d.so.4.5d` | 特征点calib3d 依赖) |
| `libopencv_flann.so.4.5d` | FLANN 索引features2d 依赖) |
### 图像编解码层OpenCV 依赖)
| 库 | 用途 |
|---|---|
| `libjpeg.so.8` | JPEG |
| `libpng16.so.16` | PNG |
| `libtiff.so.5` | TIFF含 libjbig/libdeflate/liblzma/libzstd/liblz4 |
| `libwebp.so.7` | WebP |
| `libopenjp2.so.7` | JPEG2000 |
| `libIlmImf-2_5.so.25` 等 | OpenEXRIlmImf/Imath/Iex/IlmThread/Half |
| `libheif.so.1` 等 | HEIF/HEVClibaom/libde265/libx265/libdav1d |
| `libgif.so.7` | GIF |
| `libcharls.so.2` | JPEG-LS |
| `libz.so.1` | zlib |
### 并行计算OpenCV 依赖)
| 库 | 用途 |
|---|---|
| `libtbb.so.2` | Intel TBB 线程并行 |
### DNN / 模型序列化OpenCV DNN 依赖)
| 库 | 用途 |
|---|---|
| `libprotobuf.so.23` | ProtobufONNX 模型解析) |
### GDAL / 地理空间OpenCV imgcodecs 依赖,随包但运行中不主动调用)
libgdal、libgdcm\*、libgeotiff、libproj、libgeos\*、libogdi、libspatialite、
libnetcdf、libhdf5\*、libpqPostgreSQL、libmysqlclient、libsqlite3、
libcfitsio、libkml\*、libminizip、liburiparser、libxerces-c、libfreexl、
libarmadillo、libblas/liblapack/libgfortran、libarpack、libsuperlu、
libqhull\_r、libfyba/libfyut/libfygm、libblosc、libsnappy、libsz/libaec、
libmfhdfalt/libdfalt、librttopo、libxml2、libgif
### 字体 / PDFlibgdal → libpoppler 链)
libpoppler、libfreetype、libbrotli\*、libfontconfig、liblcms2
### 网络 / 安全libgdal → libcurl 链,随包但不主动使用)
libcurl、libcurl-gnutls、libssl/libcryptoOpenSSL 3、libgnutls、
libnettle/libhogweed/libgmp、libp11-kit、libffi、libtasn1、
libgssapi\_krb5/libkrb5/libk5crypto/libkrb5supportKerberos
libcom\_err、libkeyutils、libnghttp2、libidn2、libunistring、
librtmp、libssh、libpsl、libldap/liblber、libsasl2
### 其他系统级(随包)
libicuuc/libicudataUnicode、libnss\*/libnspr4/libplc4/libplds4/libsmime3NSS
libuuid、libjson-c、libexpat、libpcre2-8、libltdl、libodbc/libodbcinst、
libnuma、libresolv、libtirpc
> 上述库来自 Ubuntu 22.04 aptOpenCV 4.5.4 的官方传递依赖闭包),均为 **release/优化版本**
> 非 debug 版。库数量约 **131 个**,打包后约 **70 MB**
> OpenCV 从源码编译(非 apt 包),**SONAME 为标准的 `libopencv_*.so.4.5`**
> (不带 Debian/Ubuntu 的 `d` 后缀),与自行源码编译 OpenCV 4.5.x 的环境兼容。
>
> 图像编解码libjpeg/libpng/libtiff/libwebp/openjpeg、protobuf、zlib
> 均已**静态链入** OpenCV不再作为独立 `.so` 出现。
---
@ -86,7 +37,6 @@ libnuma、libresolv、libtirpc
| `libpthread.so.0` | 同上(已并入 glibc 2.34+ |
| `libm.so.6` | 同上 |
| `libdl.so.2` | 同上 |
| `librt.so.1` | 同上 |
| `libstdc++.so.6` | GCC 11+Ubuntu 22.04 自带) |
| `libgcc_s.so.1` | 同上 |
| `ld-linux-aarch64.so.1` | 动态链接器,系统自带 |
@ -95,47 +45,48 @@ libnuma、libresolv、libtirpc
## 三、为什么这么设计
**问题:** OpenCV 在 Ubuntu 22.04 拖着约 131 个传递依赖(包括 GDAL、curl、Kerberos、
OpenSSL 等)。如果只靠目标板 apt 安装,新板子需要联网、可能遇到镜像源不可达、
版本不一致等问题,交付不可控。
**问题:** 如果目标板通过 apt 安装 OpenCVDebian/Ubuntu 会给 SONAME 加私有后缀
(如 `libopencv_core.so.4.5d` 而非标准的 `…4.5`),导致与自行编译的 OpenCV 不兼容;
另外 apt 安装需要联网,新板子部署不可控。
**解法:** 把整个依赖闭包随包发布。运行时通过 `LD_LIBRARY_PATH` 优先搜索 `lib/`
所有依赖在包内自洽。这些库只对本进程生效,**不替换系统库**,不影响系统
ssl/网络栈/其他程序。
**解法:** 从 OpenCV 4.5.0 官方源码编译,只构建算法实际需要的 7 个模块
core/imgproc/calib3d/imgcodecs/dnn/features2d/flann图像编解码等第三方库
全部静态链入。最终 `lib/` 只有 9 个 `.so`(含本库 + librknnrt**22 MB**
依赖关系极简,无需联网、无需 apt。
---
## 四、运行时加载方式
```bash
# 解包后,设一次环境变量,之后所有调用本库的程序都能找到这些依赖
# 解包后,设一次环境变量
export LD_LIBRARY_PATH="$(pwd)/lib:$LD_LIBRARY_PATH"
# 验证:应当没有任何 "not found" 输出
ldd lib/libstereo_bolt.so | grep "not found"
```
`LD_LIBRARY_PATH` 对所有传递依赖生效——当 `libopencv_core` 加载 `libjpeg` 时,
链接器同样优先搜索 `lib/`,无需对每个 `.so` 单独处理。
---
## 五、SONAME 兼容性说明
| 编译来源 | SONAME | 与本包兼容? |
|---|---|---|
| **本包(源码编译 4.5.0** | `libopencv_core.so.4.5` | ✅ |
| **自行源码编译 4.5.x** | `libopencv_core.so.4.5` | ✅ |
| **Ubuntu apt `libopencv-dev`** | `libopencv_core.so.4.5d` | ❌ SONAME 不匹配 |
本包的 `libstereo_bolt.so` 链接的 NEEDED 是标准 `libopencv_*.so.4.5`(无 `d`
与任何从源码编译的 OpenCV 4.5.x 兼容。如果目标板上碰巧装了 apt 的 OpenCV`d`
只要 `LD_LIBRARY_PATH` 优先指向 `lib/`,就会用本包内的库,不会冲突。
---
## 五、编译调用程序时注意
## 、编译调用程序时注意
运行不需要 OpenCV但**编译** `example/` 示例代码时需要 OpenCV **头文件**
`.h`),而 `lib/` 里只有 `.so`(运行库),没有头文件。
**生产路径**:你自己的代码只用 `include/stereo_bolt/c_api.h`,不引用任何 OpenCV 头,
相机 buffer 直接传给 `sb_process_bolt_module_buffers`**完全不需要 OpenCV 头文件**
两种方案:
**A. 目标板上 apt 装头文件(推荐,只需一次)**
```bash
# 只装 -dev 包获取头文件;运行库用包内 lib/ 的,不用 apt 装的 .so
sudo apt install libopencv-dev
```
**B. 仅使用 C ABI无需 OpenCV 头(生产路径)**
你自己的产品代码只用 `include/stereo_bolt/c_api.h`,不引用任何 OpenCV 头,
相机 buffer 直接传给 `sb_process_bolt_module_buffers`。这是生产路径,
**完全不需要 OpenCV 头文件**,也不需要 apt。
`example/` 仅作集成验证用,生产端不需要编译它。
**编译 `example/` 示例**(可选,仅用于集成验证):当前示例是纯 C
只读 headerless Mono8 raw 文件并调用 C ABI不引用 OpenCV 头文件。
运行时仍用本包 `lib/` 里的库。

View File

@ -32,9 +32,9 @@ cd stereo_bolt_delivery
```
stereo_bolt_delivery/
├── lib/ ← 算法库 + OpenCV 4.5.4 + 全部传递依赖131 个 .so
├── lib/ ← 算法库 + OpenCV 4.5.0 + librknnrt共 9 个 .so
├── include/stereo_bolt/ ← c_api.h唯一对外头文件
├── weights/ ← best_1280_20260621.rknnNPU 模型)
├── weights/ ← best_obb_1024.rknnNPU OBB 模型)
├── calib/ ← 出厂标定 XML0621 装配baseline 482mm
├── config/ ← config.rk3588.12mp.yaml兼容接口用
├── example/ ← 最小 C 示例 + CMakeLists
@ -82,20 +82,31 @@ StereoBoltModelC model = {
.data = rknn_bytes, // weights/*.rknn 的原始字节
.size = rknn_size,
.backend = SB_YOLO_BACKEND_RKNN,
.imgsz = 1280,
.imgsz = 1024,
.conf = 0.5f
};
StereoBoltParamsC p = sb_default_params();
p.wd_z_min_mm = 1200; // 工作距离下限mm
p.wd_z_max_mm = 2800; // 工作距离上限mm
p.wd_z_min_mm = 900; // 工作距离下限mm
p.wd_z_max_mm = 4000; // 工作距离上限mm
StereoBoltCtx* ctx = sb_create_ex(&calib, &model, &p); // 只调一次(慢,载模型进 NPU
```
> 兼容方式:`sb_create("config/config.rk3588.12mp.yaml")`(读文件,需工作目录正确)。
> 模型:`weights/best_1280_20260621.rknn`(精测 + 测距共用imgsz 1280)。
> 模型:`weights/best_obb_1024.rknn`(精测 + 测距共用OBB imgsz 1024)。
> 标定:`calib/stereo_calib_20260621_202857_ascii.xml`0621 装配baseline 482 mm
### 高度测量模式
默认 `height_from_plane: false`:高度 = 螺杆顶端到可见底端(**不跑平面拟合**,快 ~400ms/帧)。
如需平面模式(高度 = 顶端到支撑平面法向距离),在 config 中改为:
```yaml
measurement:
height_from_plane: true
```
开启后每帧增加 300-550ms 平面拟合耗时。
---
## 6. 每帧调用
@ -118,7 +129,7 @@ sb_free_bolt_module_result(&m); // 必须 free
StereoBoltRangeSummaryC s;
StereoBoltRangeBoltC bolts[64];
sb_range_bolt_binned(ctx, lb, bw, bh, 0, rb, bw, bh, 0, 1, &s, bolts, 64);
// s.distance_mm最近螺栓到相机的距离mm
// s.distance_mm相机到可见螺栓代表距离mm
// ── 退出 ─────────────────────────────────────────────────────────────────
sb_destroy(ctx); // 只调一次
@ -131,19 +142,15 @@ sb_destroy(ctx); // 只调一次
## 7. 编译示例程序(可选,用于集成验证)
示例代码读图用了 OpenCV编译时需要 **OpenCV 头文件**`lib/` 里只有 `.so`,没有 `.h`)。
示例代码 `main.c` 是纯 C**不依赖 OpenCV**(读 raw Mono8 文件 + C ABI 调用)。
```bash
# 安装 OpenCV 头文件(只需 -dev 包;运行库用 lib/ 里的,不用 apt 的 .so
sudo apt install libopencv-dev
# 编译
cd example
cmake -S . -B build -DSB_ROOT="$(cd .. && pwd)"
cmake --build build
```
> **生产代码不需要此步骤**:你自己的调用端只用 `c_api.h`不引用任何 OpenCV 头,
> **生产代码同理**:你自己的调用端只用 `c_api.h`
> 相机 buffer 直接喂给 `sb_process_bolt_module_buffers`,全程无 OpenCV 依赖。
---
@ -154,20 +161,13 @@ cmake --build build
# 在 stereo_bolt_delivery/ 根目录执行(使 lib/ 路径正确)
export LD_LIBRARY_PATH="$(pwd)/lib:$LD_LIBRARY_PATH"
# 精测 demo需要左右 12MP 4096×3000与标定分辨率一致
# 精测 demo需要左右 12MP raw Mono84096×3000
./example/build/bolt_demo \
calib/stereo_calib_20260621_202857_ascii.xml \
weights/best_1280_20260621.rknn \
/path/to/left.bmp /path/to/right.bmp 6
# 测距 demo
./example/build/range_demo \
calib/stereo_calib_20260621_202857_ascii.xml \
weights/best_1280_20260621.rknn \
/path/to/left.bmp /path/to/right.bmp
config/config.rk3588.12mp.yaml \
/path/to/left.raw /path/to/right.raw 4096 3000 8
```
板上实测吞吐:**精测 ~582 ms/帧 ≈ 1.7 Hz**、**测距 ~238 ms/帧 ≈ 4.2 Hz**0621 装配,帧 1)。
板上实测吞吐:**精测 ~400 ms/帧 ≈ 2.5 Hz**height_from_plane=false、**测距 ~238 ms/帧 ≈ 4.2 Hz**0621 装配)。
---

View File

@ -1,12 +1,14 @@
# ============================================================
# (C++ port)
#
# Python e:/02_Modules/StereoVisionBoltMeasurement/config.yaml
#
# Python paths
# RK3588 delivery config — OBB model, baseline 482mm
# Only used by sb_create(); sb_create_ex() ignores this file.
# ============================================================
# ---------- ----------
# ---------- working distance ----------
working_distance:
z_min_mm: 900
z_max_mm: 4000
# ---------- image ----------
image:
width: 4096
height: 3000
@ -14,35 +16,32 @@ image:
expected_dtype: "uint8"
allowed_extensions: [".tiff", ".tif", ".bmp", ".png"]
# ---------- ----------
# ---------- image validation ----------
image_validation:
min_max_value: 10
max_saturation_ratio: 0.20
# ---------- ChArUco ----------
# ---------- ChArUco ----------
charuco:
rows: 9
cols: 9
square_length_mm: 50.0
marker_length_mm: 37.0
dict_id: 11 # DICT_6X6_1000
dict_id: 11
min_corners_per_image: 30
# ---------- ----------
# ---------- calibration ----------
calibration:
source: "xml"
xml_path: "calib/stereo_calib.xml" # 0621 新装配 baseline 482mm (ChArUco)
xml_path: "calib/stereo_calib.xml"
min_image_pairs: 15
max_skip_ratio: 0.30
max_single_rms: 0.40 # : 12MP RMS=0.373(5MP 0.19)
# 0.40 ;0.373 0.1px ,,
# <0.20
max_single_rms: 0.40
max_stereo_rms: 0.80
expected_baseline_range: [80, 600]
use_fix_intrinsic: true
# ---------- ----------
# alpha=1.0 CLAUDE.md #1
# ---------- rectification ----------
rectification:
alpha: 1.0
interpolation: "cubic"
@ -53,9 +52,7 @@ rectification:
max_epipolar_error_pixel: 5.0
min_valid_roi_ratio: 0.25
# ---------- SGBM ----------
# min_disparity / num_disparities f_rectB
# f_rect3190, B235mmalpha=1.0
# ---------- SGBM ----------
sgbm:
min_disparity: 288
num_disparities: 736
@ -69,24 +66,18 @@ sgbm:
pre_filter_cap: 63
mode: "SGBM_3WAY"
# ---------- WLS ----------
# ---------- WLS ----------
wls:
lambda_value: 8000.0
sigma_color: 1.5
# ---------- ----------
# ---------- disparity validation ----------
disparity_validation:
min_valid_pixel_ratio: 0.10
min_disparity_range: 50
max_nan_ratio: 0.90
# ---------- ----------
# z_min_mm / z_max_mm / expected_wd_range are FALLBACK only when run_pipeline /
# c_api are wired (the standard path) SceneRuntime derives the real reachable
# Z range from (f_rect, baseline, sgbm.min_disparity, sgbm.num_disparities) and
# overrides these values per frame. See docs/L_100_SCENE_HANDOFF.md 6.2 and
# include/stereo_bolt/scene_runtime.hpp. Keep the fallback values conservative
# enough to cover every supported workpiece distance.
# ---------- pointcloud ----------
pointcloud:
z_min_mm: 600
z_max_mm: 3000
@ -108,13 +99,7 @@ pointcloud:
dilate_px: 100
min_area_ratio: 0.02
# ---------- 2D ----------
# mask_method: foam_mask( mask)
# "brightness" + (;
# rectify ROI 0, mask ROI )
# "ransac" dense xyz_map RANSAC ,
# " < ransac.distance_thresh_mm" mask
# "brightness" parity_tests , "ransac"
# ---------- 2D detection ----------
bolt_detect_2d:
mask_method: "ransac"
foam_brightness_threshold: 70
@ -125,99 +110,75 @@ bolt_detect_2d:
bolt_min_area_px: 800
min_bolts: 1
max_bolts: 0
bolt_exclude_dilate_px: 0 # 0 CLAUDE.md #5
bolt_exclude_dilate_px: 0
ransac:
distance_thresh_mm: 8.0 # < inlier(3
# frame 100 residual_std2.6 mm)
distance_thresh_mm: 8.0
max_iterations: 200
min_inlier_ratio: 0.15 # inlier
sample_stride: 4 # xyz_map ( 44 1 RANSAC )
morph_close_kernel_px: 9 # mask +
min_inlier_ratio: 0.15
sample_stride: 4
morph_close_kernel_px: 9
# ---------- (SVD on foam mask) ----------
# ---------- plane ----------
plane:
max_residual_std_mm: 15.0
max_normal_to_axis_deg: 60
outlier_rejection_iterations: 5
outlier_mad_k: 2.5
# ---------- ----------
# plane fit bolt extract bolt 3D
# inlier OBB, OBB margin_mm WARN
# margin_mm bolt.expected_diameter_mm,
# /
# ---------- support gate ----------
support_gate:
enabled: true
margin_mm: 10.0
# ---------- ----------
# (run_pipeline / c_api) bolt bbox_projection h_est,
# extract height max(h_est 1.3, h_est + 20 mm),
# validate_measurements |measured - h_est| < bolt.height_tolerance_mm
# max_height_mm / extract_height_filter_mm fallback
# bbox_projection h_est
# (expected_diameter_mm),
# ---------- bolt ----------
bolt:
min_height_mm: 5.0
max_height_mm: 250.0 # fallback ceiling (used only when h_est is unavailable)
extract_height_filter_mm: 200.0 # fallback filter (same condition)
height_tolerance_mm: 60.0 # |measured - h_est| ; validate_measurements
max_height_mm: 250.0
extract_height_filter_mm: 200.0
height_tolerance_mm: 60.0
expected_diameter_mm: 16.0
min_points_per_bolt: 50
max_axis_plane_angle_deg: 30
extract_fallback: "bbox_projection" # 'none' | 'bbox_projection'; YOLO bbox + , SGBM /
extract_fallback: "bbox_projection"
# ---------- ----------
# ---------- measurement ----------
measurement:
top_extraction_method: "percentile_on_axis"
top_percentile: 0.99
foot_rim_correction: false # 0621 决定: 关 rim(圆顶无领圈杆上 rim 修正会过冲)
height_from_plane: false
plane_use_sgbm: true
plane_sgbm_scale: 0.5
plane_max_samples: 40000
# ---------- Diagnostics ----------
# ---------- diagnostics ----------
diagnostics:
enabled: true
blind_strip_warn_ratio: 0.25
min_roi_pointcloud_coverage_ratio: 0.001
# ---------- YOLO ----------
# C++ ONNX Python best_v4_finetune.pt .onnx
# dilate (50/30),
# scene_runtime.cpp pixels_per_mm(= f / Z_repr),
# /
# ---------- YOLO ----------
yolo:
enabled: true
# 统一模型: 精测(sb_process_bolt_module_*)与测距(sb_range_bolt_binned)共用 weights/best.rknn。
# 当前交付模型为 960 输入;文件名保持稳定,模型更新时只替换 weights/best.rknn。
model_path: "weights/best.rknn"
backend: "rknn"
imgsz: 960
task: "auto"
imgsz: 1024
conf: 0.5
dead_zone_min_valid_pts: 50
dead_zone_bbox_dilate_mm: 24.0 # = expected_diameter_mm 1.5; valid_xyz bbox ,/
foam_bbox_dilate_mm: 16.0 # = expected_diameter_mm 1.0; foam_mask YOLO bbox
# Experimental: keep sparse-global YOLO bolts only when right YOLO + local LR check pass.
dead_zone_bbox_dilate_mm: 24.0
foam_bbox_dilate_mm: 16.0
local_stereo_fallback:
enabled: true
min_valid_points: 50
# ---------- working distance (physical, rig-level) ----------
# 0621 新装配 B=482mm: 精测视差带从 WD + 标定自推导 (disp = f*B/Z), 换基线无需重调像素。
# 旧固定 sgbm 像素带(288/736)是 200mm 装配的, 对 482mm 会 NEAR-clip 近处螺柱。
# 注意: 需 .so 含视差带解耦(commit dc696d8 起); 旧 .so 不读此块仍用固定带。
working_distance:
z_min_mm: 1200
z_max_mm: 2800
# ---------- ranging (binned) ----------
# 测距视差带也按 0621 rig 的 WD; 物理 LRC 门(mm)随距离自紧。
ranging:
z_min_mm: 1300
z_max_mm: 3600
z_min_mm: 900
z_max_mm: 4000
lrc_max_mm: 2.5
lrc_trusted_mm: 1.2
# ---------- ----------
# ---------- evaluation ----------
evaluation:
height_max_mm: 2.0
height_p95_mm: 1.5
@ -226,24 +187,23 @@ evaluation:
repeatability_std_max_mm: 1.0
matching_max_distance_mm: 50.0
# ---------- ----------
# ---------- pipeline ----------
pipeline:
fail_fast: true
resume_from_checkpoint: true
parallel_frames: false
save_intermediate: false
# ---------- ----------
# ---------- logging ----------
logging:
level: "INFO"
log_to_file: true
log_file: "outputs/logs/pipeline.log"
# ---------- ----------
# dataset/ , Python
# ---------- paths ----------
paths:
calibration_dir: "calib"
test_dir: "dataset/test"
groundtruth_csv: "dataset/groundtruth.csv"
output_dir: "outputs_macro257"
stereo_params_yaml: "outputs_macro257/stereo_params.yaml"
output_dir: "outputs"
stereo_params_yaml: "outputs/stereo_params.yaml"

View File

@ -1,21 +1,20 @@
# Minimal downstream build: link the prebuilt libstereo_bolt.so via c_api.h only.
# No OpenCV / RKNN headers needed here the C ABI hides all C++/OpenCV types.
# Minimal downstream build links the prebuilt libstereo_bolt.so.
# bolt_demo (main.c) -> sb_create + sb_process_bolt_module_buffers
#
# OpenCV here is NOT required by the demo it uses only C ABI + raw Mono8
# buffers. OpenCV is a transitive runtime dep of libstereo_bolt.so only.
cmake_minimum_required(VERSION 3.16)
project(bolt_demo LANGUAGES C)
# Point at the unpacked delivery dir (contains include/ and lib/).
set(SB_ROOT "${CMAKE_CURRENT_LIST_DIR}/.." CACHE PATH "unpacked libstereo_bolt delivery root")
add_executable(bolt_demo main.c)
target_include_directories(bolt_demo PRIVATE "${SB_ROOT}/include")
target_link_directories(bolt_demo PRIVATE "${SB_ROOT}/lib")
target_link_libraries(bolt_demo PRIVATE stereo_bolt)
target_link_options(bolt_demo PRIVATE "LINKER:--allow-shlib-undefined")
# libstereo_bolt.so pulls in OpenCV + librknnrt transitively. You do NOT need
# those at LINK time defer their resolution to runtime so you can link with
# just -lstereo_bolt. (Without this, ld follows the NEEDED chain and fails with
# "undefined reference to rknn_*/cv::*".)
target_link_options(bolt_demo PRIVATE "LINKER:--allow-shlib-undefined")
# Runtime: LD_LIBRARY_PATH must include the dirs holding
# libstereo_bolt.so + libopencv_*.so (matching ABI_FINGERPRINT.txt) + librknnrt.so
# Runtime: LD_LIBRARY_PATH must include ../lib (holds libstereo_bolt.so +
# OpenCV 4.5.0 + librknnrt.so). Example:
# LD_LIBRARY_PATH=../lib ./build/bolt_demo \
# ../config/config.rk3588.12mp.yaml L.raw R.raw 4096 3000 8

View File

@ -184,7 +184,7 @@ typedef struct {
double wd_z_max_mm;
double sgbm_min_disparity; /* fixed pixel band (used when wd_* unset) */
double sgbm_num_disparities;
int foot_rim_correction; /* -1 = built-in default, 0 = off, 1 = on */
int foot_rim_correction; /* -1 = built-in default (off), 0 = off, 1 = on */
/* --- binned ranging path (sb_range_bolt_binned) --- */
double rng_z_min_mm;
@ -259,16 +259,16 @@ typedef struct {
double distance_mm; /* straight-line distance, left optical centre -> bolt */
int left_x; /* bbox centre in the rectified-left (binned) image */
int left_y;
int right_x; /* NCC correspondence in the rectified-right (binned) image */
int right_y; /* -> the "same bolt" evidence; mark to verify */
double ncc_score; /* match score */
double lrc_px; /* left-right consistency residual (px); small == same point */
int right_x; /* matched point in the rectified-right (binned) image */
int right_y; /* OBB-axis route: right OBB axis point; NCC fallback: correspondence */
double ncc_score; /* detector/match score; ABI name kept for compatibility */
double lrc_px; /* OBB disparity stddev or NCC LRC residual; smaller is better */
int confidence; /* 0 = trusted (strong + self-consistent), 1 = suspect */
} StereoBoltRangeBoltC;
typedef struct {
int found; /* 1 if >= 1 bolt was detected and NCC/LRC-verified */
double distance_mm; /* MEDIAN straight-line distance over visible bolts */
int found; /* 1 if >= 1 bolt was detected and paired/matched */
double distance_mm; /* representative straight-line distance over visible bolts */
int bolt_count; /* total visible bolts (may exceed the number written to bolts[]) */
char status[32]; /* "ok" | "no_detection" | "no_valid_match" (why found==0) */
} StereoBoltRangeSummaryC;
@ -281,7 +281,7 @@ typedef struct {
* - The binned size may be ANY clean downscale of the calibrated size
* (config image.width/height); the binned intrinsics are derived internally
* (no separate binned calibration needed). 2x2 binning is the intended case.
* - out : summary (found / median distance / visible count). Required.
* - out : summary (found / representative distance / visible count). Required.
* - bolts : optional caller-allocated array (capacity max_bolts); filled with
* per-bolt detail (distance + left/right match points), near -> far.
* Pass NULL / max_bolts=0 to skip. The caller owns it; nothing to free.

View File

@ -4,17 +4,53 @@
TEMPLATE = subdirs
# SDK(需要先编译)
SUBDIRS += ../SDK/Device/EpicEye/EpicEyeSDK.pro
isEmpty(TARGET_APP) {
SUBDIRS += ../SDK/Device/EpicEye/EpicEyeSDK.pro
SUBDIRS += \
VrEyeDevice/VrEyeDevice.pro \
EpicEyeDevice/EpicEyeDevice.pro \
GalaxyDevice/GalaxyDevice.pro \
HikDevice/HikDevice.pro \
MvsDevice/MvsDevice.pro \
GlLineLaserDevice/GlLineLaserDevice.pro
SUBDIRS += \
VrEyeDevice/VrEyeDevice.pro \
EpicEyeDevice/EpicEyeDevice.pro \
GalaxyDevice/GalaxyDevice.pro \
HikDevice/HikDevice.pro \
MvsDevice/MvsDevice.pro \
GlLineLaserDevice/GlLineLaserDevice.pro
} else {
!equals(TARGET_APP, "BeltTearing") {
SUBDIRS += VrEyeDevice/VrEyeDevice.pro
}
contains(TARGET_APP, "^(WorkpiecePosition|WorkpieceProject)$") {
SUBDIRS += ../SDK/Device/EpicEye/EpicEyeSDK.pro
SUBDIRS += EpicEyeDevice/EpicEyeDevice.pro
SUBDIRS += VrEyeDevice/VrEyeDevice.pro
}
equals(TARGET_APP, "BinocularMark") {
SUBDIRS += GalaxyDevice/GalaxyDevice.pro
}
contains(TARGET_APP, "^(DroneScrewServer|DroneScrewbolt)$") {
SUBDIRS += MvsDevice/MvsDevice.pro
}
equals(TARGET_APP, "BagThreadPosition") {
SUBDIRS += VrEyeDevice/VrEyeDevice.pro
SUBDIRS += GlLineLaserDevice/GlLineLaserDevice.pro
}
equals(TARGET_APP, "TunnelChannel") {
SUBDIRS += VrEyeDevice/VrEyeDevice.pro
SUBDIRS += HikDevice/HikDevice.pro
}
contains(TARGET_APP, "^(BeltTearing|WorkpieceSplice|GrabBag|LapWeld|Workpiece|ParticleSize|ScrewPosition|WorkpieceHole|DiscHolePose|TireHolePose|StatorPosition|RodAndBarPosition|RodWeldSeam|HoleDetection|HolePitPosition|WheelMeasure)$") {
SUBDIRS += VrEyeDevice/VrEyeDevice.pro
}
}
win32-msvc {
SUBDIRS += RsLidarDevice/RsLidarDevice.pro
}
SUBDIRS = $$unique(SUBDIRS)