螺杆的本地仿真功能

This commit is contained in:
杰仔 2026-07-11 15:28:36 +08:00
parent 0a15ce5adb
commit 2f3f6a61fc
15 changed files with 707 additions and 58 deletions

View File

@ -104,9 +104,9 @@ DroneScrewbolt 采用远端服务端和地面端控制应用分离的结构。
| 通道 | 默认端口 | 模式 | 用途 |
| --- | ---: | --- | --- |
| UDP discovery | 5554 | 广播/应答 | 地面端搜索 DroneScrewServer |
| ZMQ control | 5555 | REQ/REP | start/stop/open/get_info/参数设置 |
| ZMQ result | 5556 | PUB/SUB | 检测结果 boxes/distances |
| ZMQ raw | 5557 | PUB/SUB | 精准检测模式下的 raw 图像 |
| ZMQ control | 15555 | REQ/REP | start/stop/open/get_info/参数设置 |
| ZMQ result | 15556 | PUB/SUB | 检测结果 boxes/distances |
| ZMQ raw | 15557 | PUB/SUB | 精准检测模式下的 raw 图像 |
| RTMP publish | 1935 | FFmpeg/FLV | DroneScrewServer 推到本机 mediamtx 的内部入口 |
| RTSP pull | 8554 | RTSP/TCP | 地面端和 VLC 从 mediamtx 拉取距离检测实时画面 |

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 798 KiB

View File

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

View File

@ -4,9 +4,9 @@
<!-- 板一 (Server) 连接参数 -->
<!-- zmqRawImage=0 表示不订阅原始图像 PUB -->
<Server ip="192.168.1.10"
zmqControl="5555"
zmqResult="5556"
zmqRawImage="5557"
zmqControl="15555"
zmqResult="15556"
zmqRawImage="15557"
rtspPort="8554"
rtspPath="/live/dronescrew"
rtspUser=""

View File

@ -216,11 +216,13 @@ MainWindow::MainWindow(QWidget* parent)
.arg(MINUTE, 2, 10, QChar('0'))
.arg(SECOND, 2, 10, QChar('0'));
QLabel* buildLabel = new QLabel(versionWithBuildTime);
buildLabel->setStyleSheet("QLabel { color: rgb(221, 225, 233); font-size: 20px; margin-right: 16px; }");
QLabel* buildLabel = new QLabel(versionWithBuildTime, statusBar());
buildLabel->setStyleSheet("QLabel { color: rgb(221, 225, 233); font-size: 20px; background-color: transparent; }");
buildLabel->setCursor(Qt::PointingHandCursor);
buildLabel->installEventFilter(this);
statusBar()->addPermanentWidget(buildLabel);
buildLabel->adjustSize();
buildLabel->show();
statusBar()->setMinimumHeight(buildLabel->sizeHint().height() + 10);
m_versionLabel = buildLabel;
initUi();
@ -228,6 +230,7 @@ MainWindow::MainWindow(QWidget* parent)
// 无边框 + 最大化
setWindowFlags(Qt::FramelessWindowHint);
this->showMaximized();
updateVersionLabelAlignment();
// 第二相机画面初始隐藏
ui->detect_image_2->setVisible(false);
@ -696,6 +699,7 @@ void MainWindow::resizeEvent(QResizeEvent* event)
{
QMainWindow::resizeEvent(event);
renderDisplay();
updateVersionLabelAlignment();
}
void MainWindow::renderDisplay()
@ -1002,6 +1006,7 @@ void MainWindow::setupSingleCameraLayout()
ui->frame_dev, ui->detect_log,
availableHeight);
m_deviceStatusWidget->setCameraCount(1);
updateVersionLabelAlignment();
appendLog(QStringLiteral("页面布局已调整为单相机模式"));
}
@ -1014,9 +1019,33 @@ void MainWindow::setupMultiCameraLayout()
ui->frame_dev, ui->detect_log,
availableHeight);
m_deviceStatusWidget->setCameraCount(2);
updateVersionLabelAlignment();
appendLog(QStringLiteral("页面布局已调整为双相机模式"));
}
void MainWindow::updateVersionLabelAlignment()
{
if (!m_versionLabel || !ui || !ui->detect_log || !statusBar())
return;
const int targetRight = ui->detect_log->mapTo(this, QPoint(ui->detect_log->width(), 0)).x();
const int statusRight = statusBar()->mapTo(this, QPoint(statusBar()->width(), 0)).x();
int rightMargin = statusRight - targetRight;
if (rightMargin < 0)
rightMargin = 0;
const QSize hint = m_versionLabel->sizeHint();
m_versionLabel->resize(hint);
int x = statusBar()->width() - rightMargin - hint.width();
if (x < 0)
x = 0;
int y = (statusBar()->height() - hint.height()) / 2;
if (y < 0)
y = 0;
m_versionLabel->move(x, y);
m_versionLabel->raise();
}
// ======================== 事件过滤器 ========================
bool MainWindow::eventFilter(QObject* obj, QEvent* event)

View File

@ -77,6 +77,7 @@ private:
void setupSingleCameraLayout();
void setupMultiCameraLayout();
int getAvailableHeight();
void updateVersionLabelAlignment();
private:
Ui::MainWindow* ui{nullptr};

View File

@ -10,9 +10,9 @@
struct DroneScrewServerInfo
{
std::string serverIp{"192.168.1.10"}; // 板一 IP
int zmqControlPort{5555}; // REQ-REP 控制
int zmqResultPort{5556}; // PUB-SUB 检测结果
int zmqRawImagePort{5557}; // PUB-SUB 原始图像,0=不订阅
int zmqControlPort{15555}; // REQ-REP 控制
int zmqResultPort{15556}; // PUB-SUB 检测结果
int zmqRawImagePort{15557}; // PUB-SUB 原始图像,0=不订阅
int rtspPort{8554}; // RTSP
std::string rtspPath{"/live/dronescrew"};
std::string rtspUser;

View File

@ -46,6 +46,25 @@ static bool BoolAttr(const XMLElement* e, const char* name, bool dft)
return v;
}
constexpr int kDefaultZmqControlPort = 15555;
constexpr int kDefaultZmqResultPort = 15556;
constexpr int kDefaultZmqRawImagePort = 15557;
constexpr int kLegacyZmqControlPort = 5555;
constexpr int kLegacyZmqResultPort = 5556;
constexpr int kLegacyZmqRawImagePort = 5557;
static void NormalizeServerZmqPorts(DroneScrewServerInfo& server)
{
if (server.zmqControlPort <= 0 || server.zmqControlPort == kLegacyZmqControlPort)
server.zmqControlPort = kDefaultZmqControlPort;
if (server.zmqResultPort <= 0 || server.zmqResultPort == kLegacyZmqResultPort)
server.zmqResultPort = kDefaultZmqResultPort;
if (server.zmqRawImagePort == kLegacyZmqRawImagePort)
server.zmqRawImagePort = kDefaultZmqRawImagePort;
if (server.zmqRawImagePort < 0)
server.zmqRawImagePort = 0;
}
int CVrDroneScrewCtrlConfig::LoadConfig(const std::string& filePath,
DroneScrewCtrlConfigResult& result)
{
@ -69,6 +88,7 @@ int CVrDroneScrewCtrlConfig::LoadConfig(const std::string& filePath,
result.server.rtspPass = SafeAttr(s, "rtspPass", result.server.rtspPass);
result.server.rtspUseTcp = BoolAttr(s, "rtspUseTcp", result.server.rtspUseTcp);
}
NormalizeServerZmqPorts(result.server);
if (XMLElement* a = root->FirstChildElement("Algorithm"))
{
result.algo.scoreThreshold = FloatAttr(a, "scoreThreshold", result.algo.scoreThreshold);

View File

@ -5,21 +5,25 @@
#include <QCoreApplication>
#include <QDir>
#include <QDirIterator>
#include <QDomDocument>
#include <QFile>
#include <QFileInfo>
#include <QImage>
#include <QJsonArray>
#include <QNetworkInterface>
#include <QRegularExpression>
#include <QStringList>
#include <QTextStream>
#include <QXmlStreamReader>
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <exception>
#include <limits>
#include <map>
#include <memory>
#include <new>
#include <sstream>
@ -68,6 +72,12 @@ constexpr int64_t kDistanceDecimationHorizontal = 4;
constexpr int64_t kDistanceDecimationVertical = 4;
constexpr int kDetectPipelinePrecision = 0;
constexpr int kDetectPipelineDistance = 1;
constexpr int kDefaultZmqControlPort = 15555;
constexpr int kDefaultZmqResultPort = 15556;
constexpr int kDefaultZmqRawImagePort = 15557;
constexpr int kLegacyZmqControlPort = 5555;
constexpr int kLegacyZmqResultPort = 5556;
constexpr int kLegacyZmqRawImagePort = 5557;
constexpr const char* kImageSaveRootDir = "/home/cat";
constexpr size_t kMaxImageSaveQueueDepth = 100;
constexpr size_t kImageSaveWorkerCount = 4;
@ -664,6 +674,83 @@ QString resolveCandidatePath(const QString& path, const QStringList& baseDirs)
return fallback.isEmpty() ? QFileInfo(trimmed).absoluteFilePath() : fallback;
}
bool loadMono8ImageFile(const QString& path,
std::vector<unsigned char>& data,
unsigned int& width,
unsigned int& height)
{
QImage image(path);
if (image.isNull())
{
LOG_WARN("[SIM] load image failed: %s\n", path.toStdString().c_str());
return false;
}
QImage gray = image.convertToFormat(QImage::Format_Grayscale8);
if (gray.isNull() || gray.width() <= 0 || gray.height() <= 0)
{
LOG_WARN("[SIM] convert image to Mono8 failed: %s\n",
path.toStdString().c_str());
return false;
}
const uint64_t imageSize =
static_cast<uint64_t>(gray.width()) * static_cast<uint64_t>(gray.height());
if (imageSize > static_cast<uint64_t>(std::numeric_limits<unsigned int>::max()))
{
LOG_WARN("[SIM] image too large: %s size=%llu\n",
path.toStdString().c_str(),
static_cast<unsigned long long>(imageSize));
return false;
}
width = static_cast<unsigned int>(gray.width());
height = static_cast<unsigned int>(gray.height());
data.resize(static_cast<size_t>(imageSize));
for (int y = 0; y < gray.height(); ++y)
{
std::memcpy(data.data() + static_cast<size_t>(y) * width,
gray.constScanLine(y),
width);
}
return true;
}
bool resizeMono8Nearest(const std::vector<unsigned char>& src,
unsigned int srcWidth,
unsigned int srcHeight,
unsigned int dstWidth,
unsigned int dstHeight,
std::vector<unsigned char>& dst)
{
if (srcWidth == 0 || srcHeight == 0 || dstWidth == 0 || dstHeight == 0)
return false;
const uint64_t srcSize =
static_cast<uint64_t>(srcWidth) * static_cast<uint64_t>(srcHeight);
const uint64_t dstSize =
static_cast<uint64_t>(dstWidth) * static_cast<uint64_t>(dstHeight);
if (src.size() < srcSize ||
dstSize > static_cast<uint64_t>(std::numeric_limits<size_t>::max()))
return false;
dst.resize(static_cast<size_t>(dstSize));
for (unsigned int y = 0; y < dstHeight; ++y)
{
const unsigned int srcY =
static_cast<unsigned int>((static_cast<uint64_t>(y) * srcHeight) / dstHeight);
const unsigned char* srcRow = src.data() + static_cast<size_t>(srcY) * srcWidth;
unsigned char* dstRow = dst.data() + static_cast<size_t>(y) * dstWidth;
for (unsigned int x = 0; x < dstWidth; ++x)
{
const unsigned int srcX =
static_cast<unsigned int>((static_cast<uint64_t>(x) * srcWidth) / dstWidth);
dstRow[x] = srcRow[srcX];
}
}
return true;
}
QString yamlValueAfterColon(QString line)
{
const int comment = line.indexOf('#');
@ -861,6 +948,28 @@ DroneScrewServerPresenter::~DroneScrewServerPresenter()
}
}
void DroneScrewServerPresenter::setSimulationImageDir(const QString& dirPath)
{
std::lock_guard<std::mutex> lk(m_simulationImageMutex);
const QString trimmed = dirPath.trimmed();
m_simulationImageDir = trimmed.isEmpty() ? QString() : QDir(trimmed).absolutePath();
m_simulationImagePairs.clear();
m_simulationImageCursor = 0;
m_simulationFrameId = 0;
if (m_simulationImageDir.isEmpty())
LOG_INFO("[SIM] image directory simulation disabled\n");
else
LOG_INFO("[SIM] image directory simulation enabled: %s\n",
m_simulationImageDir.toStdString().c_str());
}
bool DroneScrewServerPresenter::isSimulationMode() const
{
std::lock_guard<std::mutex> lk(m_simulationImageMutex);
return !m_simulationImageDir.trimmed().isEmpty();
}
bool DroneScrewServerPresenter::loadConfiguration(const QString& configFilePath)
{
m_configFilePath = configFilePath;
@ -920,12 +1029,20 @@ bool DroneScrewServerPresenter::loadConfiguration(const QString& configFilePath)
else if (n == "Zmq")
{
auto a = xml.attributes();
m_zmqControlPort = a.value("controlPort").toInt();
m_zmqResultPort = a.value("resultPort").toInt();
m_zmqRawImagePort = a.value("rawImagePort").toInt();
if (!m_zmqControlPort) m_zmqControlPort = 5555;
if (!m_zmqResultPort) m_zmqResultPort = 5556;
if (m_zmqRawImagePort < 0) m_zmqRawImagePort = 0;
if (a.hasAttribute(QStringLiteral("controlPort")))
m_zmqControlPort = a.value(QStringLiteral("controlPort")).toInt();
if (a.hasAttribute(QStringLiteral("resultPort")))
m_zmqResultPort = a.value(QStringLiteral("resultPort")).toInt();
if (a.hasAttribute(QStringLiteral("rawImagePort")))
m_zmqRawImagePort = a.value(QStringLiteral("rawImagePort")).toInt();
if (m_zmqControlPort <= 0 || m_zmqControlPort == kLegacyZmqControlPort)
m_zmqControlPort = kDefaultZmqControlPort;
if (m_zmqResultPort <= 0 || m_zmqResultPort == kLegacyZmqResultPort)
m_zmqResultPort = kDefaultZmqResultPort;
if (m_zmqRawImagePort == kLegacyZmqRawImagePort)
m_zmqRawImagePort = kDefaultZmqRawImagePort;
if (m_zmqRawImagePort < 0)
m_zmqRawImagePort = 0;
}
else if (n == "Rtsp")
{
@ -1029,7 +1146,12 @@ int DroneScrewServerPresenter::initAll()
m_pushBitrateKbps, m_liveStreamCameraRole.c_str());
// 3) 相机
if (tryConnectCamera() != 0)
if (isSimulationMode())
{
LOG_INFO("[SIM] simulation mode enabled, skip camera connect\n");
m_bCameraConnected = false;
}
else if (tryConnectCamera() != 0)
{
LOG_WARN("Camera connect fail, start auto-reconnect timer\n");
m_pReconnectTimer->start();
@ -1044,6 +1166,7 @@ void DroneScrewServerPresenter::deinitAll()
stopLiveStream();
stopGpioTriggerLoop();
stopImageSaveThread();
clearSimulationImages();
if (m_pReconnectTimer && m_pReconnectTimer->isActive())
m_pReconnectTimer->stop();
@ -1061,6 +1184,13 @@ void DroneScrewServerPresenter::deinitAll()
int DroneScrewServerPresenter::tryConnectCamera()
{
if (isSimulationMode())
{
LOG_INFO("[SIM] skip camera connect in simulation mode\n");
m_bCameraConnected = false;
return ERR_CODE(DEV_UNSUPPORT);
}
LOG_DEBUG("========== 开始连接双目相机 ==========\n");
if (m_pLeftCamera) closeCamera();
@ -1983,6 +2113,13 @@ QJsonObject DroneScrewServerPresenter::getRuntimeInfo() const
info["detectTriggerMode"] = m_useIoTrigger ? "io" : "off";
info["streamTriggerMode"] = m_useIoTrigger ? "io" : "off";
info["detectMode"] = QString::fromStdString(detectMode());
{
std::lock_guard<std::mutex> lk(m_simulationImageMutex);
const bool simulationMode = !m_simulationImageDir.trimmed().isEmpty();
info["simulationMode"] = simulationMode;
info["simulationImageDir"] = m_simulationImageDir;
info["simulationImagePairs"] = static_cast<int>(m_simulationImagePairs.size());
}
info["mode"] = m_bLiveStreaming.load()
? "live_stream"
: (m_bIsDetecting.load() ? "detection" : "idle");
@ -2187,7 +2324,17 @@ int DroneScrewServerPresenter::startDetectionWork()
return ERR_CODE(DRONESCREW_ERR_MODE_CONFLICT);
}
if (!m_pLeftCamera || !m_pRightCamera)
const bool simulationMode = isSimulationMode();
if (simulationMode)
{
const int simRet = prepareSimulationImages(true);
if (simRet != 0)
{
emit statusChanged(QStringLiteral("Simulation image pairs not found"));
return simRet;
}
}
else if (!m_pLeftCamera || !m_pRightCamera)
{
LOG_DEBUG("Cameras not connected\n");
emit statusChanged(QStringLiteral("双目相机未连接"));
@ -2198,6 +2345,8 @@ int DroneScrewServerPresenter::startDetectionWork()
m_detectPipelineMode = kDetectPipelinePrecision;
m_activeTriggerFps = static_cast<int>(kPrecisionFrameRate);
if (!simulationMode)
{
const int acqRet = prepareDetectionAcquisition(kPrecisionFrameRate,
kPrecisionBinningHorizontal,
kPrecisionBinningVertical,
@ -2211,6 +2360,7 @@ int DroneScrewServerPresenter::startDetectionWork()
}
// 停止 RTSP 推流(检测和推流互斥)
}
if (m_pPusher)
{
LOG_DEBUG("[DETECT] stopping RTSP pusher (detection mode)\n");
@ -2222,11 +2372,14 @@ int DroneScrewServerPresenter::startDetectionWork()
m_bThreadExit = false;
m_bIsDetecting = true;
startImageSaveThread(imageSaveModeName());
startGpioTriggerLoop();
if (!simulationMode)
startGpioTriggerLoop();
m_detectThread = std::thread(&DroneScrewServerPresenter::detectThreadFunc, this);
emit statusChanged(QStringLiteral("开始持续检测(双目)"));
LOG_INFO("Detection started (binocular mode, fps=%.1f trigger=%s)\n",
kPrecisionFrameRate, m_useIoTrigger ? "io" : "off");
LOG_INFO("Detection started (%s mode, fps=%.1f trigger=%s)\n",
simulationMode ? "simulation" : "binocular",
kPrecisionFrameRate,
simulationMode ? "simulation" : (m_useIoTrigger ? "io" : "off"));
return 0;
}
@ -2243,6 +2396,7 @@ int DroneScrewServerPresenter::stopDetectionWork()
{
stopGpioTriggerLoop();
stopImageSaveThread();
clearSimulationImages();
clearLastFinalDetectionResult();
LOG_WARN("Detection not started\n");
return 0;
@ -2268,6 +2422,7 @@ int DroneScrewServerPresenter::stopDetectionWork()
}
stopImageSaveThread();
clearSimulationImages();
if (hasFinalResult)
{
finalResult.isFinalResult = true;
@ -2303,6 +2458,8 @@ int DroneScrewServerPresenter::stopDetectionWork()
int DroneScrewServerPresenter::startLiveStream()
{
const bool simulationMode = isSimulationMode();
if (m_bLiveStreaming.load())
{
LOG_DEBUG("[LIVE] already started\n");
@ -2318,15 +2475,15 @@ int DroneScrewServerPresenter::startLiveStream()
return ERR_CODE(DRONESCREW_ERR_MODE_CONFLICT);
}
IMvsDevice* streamCamera = liveStreamCamera();
const char* streamRole = liveStreamCameraRole();
auto stopLiveCameras = [this]() {
if (m_pLeftCamera && m_pLeftCamera->IsAcquisitioning())
m_pLeftCamera->StopAcquisition();
if (m_pRightCamera && m_pRightCamera->IsAcquisitioning())
m_pRightCamera->StopAcquisition();
};
if (!streamCamera)
IMvsDevice* streamCamera = simulationMode ? nullptr : liveStreamCamera();
const char* streamRole = simulationMode ? "simulation" : liveStreamCameraRole();
if (!simulationMode && !streamCamera)
{
LOG_DEBUG("[LIVE] stream camera role=%s not connected\n", streamRole);
emit statusChanged(QStringLiteral("实时传图相机未连接"));
@ -2336,19 +2493,34 @@ int DroneScrewServerPresenter::startLiveStream()
m_detectPipelineMode = kDetectPipelineDistance;
m_activeTriggerFps = static_cast<int>(kDistanceFrameRate);
const int acqRet = prepareDetectionAcquisition(kDistanceFrameRate,
kDistanceBinningHorizontal,
kDistanceBinningVertical,
kDistanceDecimationHorizontal,
kDistanceDecimationVertical,
"DISTANCE");
if (acqRet != 0)
if (simulationMode)
{
emit statusChanged(QStringLiteral("实时传图相机采集启动失败"));
releaseRtspPusher();
m_detectPipelineMode = kDetectPipelinePrecision;
m_activeTriggerFps = static_cast<int>(kPrecisionFrameRate);
return acqRet;
const int simRet = prepareSimulationImages(true);
if (simRet != 0)
{
emit statusChanged(QStringLiteral("仿真图像对加载失败"));
releaseRtspPusher();
m_detectPipelineMode = kDetectPipelinePrecision;
m_activeTriggerFps = static_cast<int>(kPrecisionFrameRate);
return simRet;
}
}
else
{
const int acqRet = prepareDetectionAcquisition(kDistanceFrameRate,
kDistanceBinningHorizontal,
kDistanceBinningVertical,
kDistanceDecimationHorizontal,
kDistanceDecimationVertical,
"DISTANCE");
if (acqRet != 0)
{
emit statusChanged(QStringLiteral("实时传图相机采集启动失败"));
releaseRtspPusher();
m_detectPipelineMode = kDetectPipelinePrecision;
m_activeTriggerFps = static_cast<int>(kPrecisionFrameRate);
return acqRet;
}
}
const unsigned int outWidth = alignEven(m_liveStreamWidth > 0 ? m_liveStreamWidth : kLiveStreamWidth);
@ -2384,10 +2556,13 @@ int DroneScrewServerPresenter::startLiveStream()
m_bLiveStreaming = true;
m_bThreadExit = false;
m_bIsDetecting = true;
startGpioTriggerLoop();
if (!simulationMode)
startGpioTriggerLoop();
m_detectThread = std::thread(&DroneScrewServerPresenter::detectThreadFunc, this);
emit statusChanged(QStringLiteral("开始实时传图"));
LOG_INFO("[LIVE] started streamCamera=%s fps=%d out=%ux%u url=%s\n",
emit statusChanged(simulationMode ? QStringLiteral("开始仿真实时传图")
: QStringLiteral("开始实时传图"));
LOG_INFO("[LIVE] started mode=%s streamCamera=%s fps=%d out=%ux%u url=%s\n",
simulationMode ? "simulation" : "camera",
streamRole, m_liveStreamFps, m_rtspWidth, m_rtspHeight,
m_rtspAdvertiseUrl.toStdString().c_str());
return 0;
@ -2412,6 +2587,8 @@ int DroneScrewServerPresenter::stopLiveStream()
if (m_detectThread.joinable())
m_detectThread.join();
stopImageSaveThread();
if (isSimulationMode())
clearSimulationImages();
m_bIsDetecting = false;
m_detectPipelineMode = kDetectPipelinePrecision;
m_activeTriggerFps = static_cast<int>(kPrecisionFrameRate);
@ -2458,6 +2635,12 @@ int DroneScrewServerPresenter::stopLiveStream()
int DroneScrewServerPresenter::swapCameraRoles()
{
if (isSimulationMode())
{
LOG_INFO("[SIM] swap camera roles ignored in simulation mode\n");
return ERR_CODE(DEV_UNSUPPORT);
}
if (m_bIsDetecting.load() || m_bLiveStreaming.load())
{
LOG_WARN("[CAM] swap rejected: stop detection/live stream first\n");
@ -2799,6 +2982,255 @@ bool DroneScrewServerPresenter::waitAndCopyLatestBinocular(MvsImageData& leftImg
return false;
}
int DroneScrewServerPresenter::prepareSimulationImages(bool forceReload)
{
std::lock_guard<std::mutex> lk(m_simulationImageMutex);
if (!forceReload && !m_simulationImagePairs.empty())
return 0;
m_simulationImagePairs.clear();
m_simulationImageCursor = 0;
m_simulationFrameId = 0;
const QString simulationDir = m_simulationImageDir.trimmed();
const QFileInfo rootInfo(simulationDir);
if (simulationDir.isEmpty() || !rootInfo.exists() || !rootInfo.isDir())
{
LOG_ERROR("[SIM] invalid simulation image dir: %s\n",
simulationDir.toStdString().c_str());
return ERR_CODE(FILE_ERR_NOEXIST);
}
const QRegularExpression fileNameRegex(
QStringLiteral("^(\\d+)_(left|right)Image\\.(png|jpg|jpeg|bmp|tif|tiff)$"),
QRegularExpression::CaseInsensitiveOption);
std::map<std::string, SimulationImagePair> pairMap;
const QString rootDir = rootInfo.absoluteFilePath();
{
QDirIterator it(rootDir,
QDir::Files | QDir::NoDotAndDotDot,
QDirIterator::Subdirectories);
while (it.hasNext())
{
it.next();
const QFileInfo fileInfo = it.fileInfo();
const QRegularExpressionMatch match =
fileNameRegex.match(fileInfo.fileName());
if (!match.hasMatch())
continue;
bool ok = false;
const unsigned long long index = match.captured(1).toULongLong(&ok);
if (!ok || index == 0)
continue;
const QString dirPath = QDir(fileInfo.absolutePath()).absolutePath();
const std::string key =
dirPath.toStdString() + "\n" + std::to_string(index);
SimulationImagePair& pair = pairMap[key];
pair.dirPath = dirPath;
pair.index = index;
const QString role = match.captured(2).toLower();
if (role == QStringLiteral("left"))
pair.leftPath = fileInfo.absoluteFilePath();
else
pair.rightPath = fileInfo.absoluteFilePath();
}
}
for (const auto& item : pairMap)
{
const SimulationImagePair& pair = item.second;
if (!pair.leftPath.isEmpty() && !pair.rightPath.isEmpty())
{
m_simulationImagePairs.push_back(pair);
}
else
{
LOG_WARN("[SIM] skip incomplete pair dir=%s index=%llu left=%s right=%s\n",
pair.dirPath.toStdString().c_str(),
pair.index,
pair.leftPath.toStdString().c_str(),
pair.rightPath.toStdString().c_str());
}
}
std::sort(m_simulationImagePairs.begin(),
m_simulationImagePairs.end(),
[](const SimulationImagePair& a, const SimulationImagePair& b) {
const int dirCmp =
QString::compare(a.dirPath, b.dirPath, Qt::CaseInsensitive);
if (dirCmp != 0)
return dirCmp < 0;
return a.index < b.index;
});
if (m_simulationImagePairs.empty())
{
LOG_ERROR("[SIM] no image pairs found under: %s, expected names like 1_leftImage.png and 1_rightImage.png\n",
rootDir.toStdString().c_str());
return ERR_CODE(FILE_ERR_NOEXIST);
}
LOG_INFO("[SIM] loaded image pair list count=%zu root=%s first=%s/%llu\n",
m_simulationImagePairs.size(),
rootDir.toStdString().c_str(),
m_simulationImagePairs.front().dirPath.toStdString().c_str(),
m_simulationImagePairs.front().index);
return 0;
}
void DroneScrewServerPresenter::clearSimulationImages()
{
std::lock_guard<std::mutex> lk(m_simulationImageMutex);
m_simulationImagePairs.clear();
m_simulationImageCursor = 0;
m_simulationFrameId = 0;
}
bool DroneScrewServerPresenter::loadNextSimulationImagePair(MvsImageData& leftImg,
MvsImageData& rightImg,
unsigned int targetWidth,
unsigned int targetHeight)
{
SimulationImagePair pair;
size_t pairOffset = 0;
size_t pairCount = 0;
unsigned long long frameId = 0;
{
std::lock_guard<std::mutex> lk(m_simulationImageMutex);
if (m_simulationImagePairs.empty())
return false;
pairCount = m_simulationImagePairs.size();
pairOffset = m_simulationImageCursor % pairCount;
pair = m_simulationImagePairs[pairOffset];
m_simulationImageCursor = (pairOffset + 1) % pairCount;
frameId = ++m_simulationFrameId;
}
std::vector<unsigned char> leftData;
std::vector<unsigned char> rightData;
unsigned int leftWidth = 0;
unsigned int leftHeight = 0;
unsigned int rightWidth = 0;
unsigned int rightHeight = 0;
if (!loadMono8ImageFile(pair.leftPath, leftData, leftWidth, leftHeight) ||
!loadMono8ImageFile(pair.rightPath, rightData, rightWidth, rightHeight))
{
LOG_WARN("[SIM] skip unreadable pair offset=%zu index=%llu\n",
pairOffset, pair.index);
return false;
}
if (leftWidth != rightWidth || leftHeight != rightHeight)
{
LOG_WARN("[SIM] skip size mismatch pair index=%llu left=%ux%u right=%ux%u\n",
pair.index, leftWidth, leftHeight, rightWidth, rightHeight);
return false;
}
const unsigned int sourceWidth = leftWidth;
const unsigned int sourceHeight = leftHeight;
if ((targetWidth == 0) != (targetHeight == 0))
{
LOG_WARN("[SIM] invalid target size pair index=%llu target=%ux%u\n",
pair.index, targetWidth, targetHeight);
return false;
}
if (targetWidth > 0 && targetHeight > 0 &&
(leftWidth != targetWidth || leftHeight != targetHeight))
{
const uint64_t targetSize =
static_cast<uint64_t>(targetWidth) * static_cast<uint64_t>(targetHeight);
if (targetSize > static_cast<uint64_t>(std::numeric_limits<unsigned int>::max()))
{
LOG_WARN("[SIM] target image too large pair index=%llu target=%ux%u\n",
pair.index, targetWidth, targetHeight);
return false;
}
std::vector<unsigned char> resizedLeft;
std::vector<unsigned char> resizedRight;
if (!resizeMono8Nearest(leftData, leftWidth, leftHeight,
targetWidth, targetHeight, resizedLeft) ||
!resizeMono8Nearest(rightData, rightWidth, rightHeight,
targetWidth, targetHeight, resizedRight))
{
LOG_WARN("[SIM] resize image pair failed index=%llu source=%ux%u target=%ux%u\n",
pair.index, leftWidth, leftHeight, targetWidth, targetHeight);
return false;
}
leftData.swap(resizedLeft);
rightData.swap(resizedRight);
leftWidth = targetWidth;
leftHeight = targetHeight;
rightWidth = targetWidth;
rightHeight = targetHeight;
}
if (leftData.size() > std::numeric_limits<unsigned int>::max() ||
rightData.size() > std::numeric_limits<unsigned int>::max())
{
LOG_ERROR("[SIM] image pair too large after resize index=%llu left=%zu right=%zu\n",
pair.index, leftData.size(), rightData.size());
return false;
}
std::unique_ptr<unsigned char[]> leftBuf(new (std::nothrow) unsigned char[leftData.size()]);
std::unique_ptr<unsigned char[]> rightBuf(new (std::nothrow) unsigned char[rightData.size()]);
if (!leftBuf || !rightBuf)
{
LOG_ERROR("[SIM] alloc image pair failed index=%llu left=%zu right=%zu\n",
pair.index, leftData.size(), rightData.size());
return false;
}
std::memcpy(leftBuf.get(), leftData.data(), leftData.size());
std::memcpy(rightBuf.get(), rightData.data(), rightData.size());
const unsigned long long timestampUs =
static_cast<unsigned long long>(QDateTime::currentMSecsSinceEpoch()) * 1000ull;
leftImg = MvsImageData{};
leftImg.pData = leftBuf.release();
leftImg.width = leftWidth;
leftImg.height = leftHeight;
leftImg.dataSize = static_cast<unsigned int>(leftData.size());
leftImg.pixelFormat = kMvsPixelTypeMono8;
leftImg.frameID = frameId;
leftImg.timestamp = timestampUs;
rightImg = MvsImageData{};
rightImg.pData = rightBuf.release();
rightImg.width = rightWidth;
rightImg.height = rightHeight;
rightImg.dataSize = static_cast<unsigned int>(rightData.size());
rightImg.pixelFormat = kMvsPixelTypeMono8;
rightImg.frameID = frameId;
rightImg.timestamp = timestampUs;
if (frameId == 1 || frameId % 100 == 0)
{
LOG_INFO("[SIM] frame=%llu pair=%zu/%zu index=%llu left=%s right=%s source=%ux%u size=%ux%u\n",
frameId,
pairOffset + 1,
pairCount,
pair.index,
pair.leftPath.toStdString().c_str(),
pair.rightPath.toStdString().c_str(),
sourceWidth,
sourceHeight,
leftWidth,
leftHeight);
}
return true;
}
QString DroneScrewServerPresenter::imageSaveModeName() const
{
if (m_detectPipelineMode.load() == kDetectPipelineDistance)
@ -3033,7 +3465,8 @@ DroneScrewResult DroneScrewServerPresenter::runSingleDetection()
return result;
};
if (!m_pLeftCamera || !m_pRightCamera)
const bool simulationMode = isSimulationMode();
if (!simulationMode && (!m_pLeftCamera || !m_pRightCamera))
{
return fail(ERR_CODE(DRONESCREW_ERR_CAMERA_NOT_CONNECTED), "cameras not connected");
}
@ -3043,7 +3476,15 @@ DroneScrewResult DroneScrewServerPresenter::runSingleDetection()
MvsImageData rightImg{};
bool usedCachedFrame = false;
if (m_bIsDetecting.load())
if (simulationMode)
{
const int simRet = prepareSimulationImages(false);
if (simRet != 0)
return fail(simRet, "simulation image pairs not found");
if (!loadNextSimulationImagePair(leftImg, rightImg))
return fail(ERR_CODE(FILE_ERR_READ), "load simulation image pair failed");
}
else if (m_bIsDetecting.load())
{
QMutexLocker lk(&m_frameMutex);
if (!m_leftImageData.pData || m_leftImageData.dataSize == 0 ||
@ -3164,8 +3605,10 @@ DroneScrewResult DroneScrewServerPresenter::runSingleDetection()
releaseImage(leftImg);
releaseImage(rightImg);
const char* sourceName =
simulationMode ? "simulation" : (usedCachedFrame ? "cache" : "callback");
LOG_INFO("[SINGLE] completed frame=%llu source=%s success=%d code=%d\n",
result.frameId, usedCachedFrame ? "cache" : "callback",
result.frameId, sourceName,
result.success ? 1 : 0, result.errorCode);
return result;
}
@ -3307,10 +3750,13 @@ void DroneScrewServerPresenter::handleUpdateAlgoParams(const DroneScrewAlgoParam
void DroneScrewServerPresenter::detectThreadFunc()
{
const bool simulationMode = isSimulationMode();
{
std::ostringstream oss;
oss << std::this_thread::get_id();
LOG_DEBUG("[DETECT] thread started (binocular mode) tid=%s\n", oss.str().c_str());
LOG_DEBUG("[DETECT] thread started (%s mode) tid=%s\n",
simulationMode ? "simulation" : "binocular",
oss.str().c_str());
}
int frameCnt = 0;
@ -3326,6 +3772,47 @@ void DroneScrewServerPresenter::detectThreadFunc()
try
{
// 等待双目图像都准备好
MvsImageData leftImg{};
MvsImageData rightImg{};
std::unique_ptr<unsigned char[]> leftBuf;
std::unique_ptr<unsigned char[]> rightBuf;
if (simulationMode)
{
const bool liveStreamDistance =
m_bLiveStreaming.load() &&
m_detectPipelineMode.load() == kDetectPipelineDistance;
const unsigned int simTargetWidth =
liveStreamDistance
? (m_rtspWidth > 0
? m_rtspWidth
: alignEven(m_liveStreamWidth > 0 ? m_liveStreamWidth
: kLiveStreamWidth))
: 0;
const unsigned int simTargetHeight =
liveStreamDistance
? (m_rtspHeight > 0
? m_rtspHeight
: alignEven(m_liveStreamHeight > 0 ? m_liveStreamHeight
: kLiveStreamHeight))
: 0;
if (!loadNextSimulationImagePair(leftImg, rightImg,
simTargetWidth, simTargetHeight))
{
const auto now = std::chrono::steady_clock::now();
if (now >= nextWaitLog)
{
LOG_WARN("[SIM] waiting readable image pair\n");
nextWaitLog = now + std::chrono::seconds(1);
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
continue;
}
leftBuf.reset(leftImg.pData);
rightBuf.reset(rightImg.pData);
}
else
{
bool leftReady = false;
bool rightReady = false;
bool pairValid = false;
@ -3334,9 +3821,6 @@ void DroneScrewServerPresenter::detectThreadFunc()
unsigned long long currentRightFrameId = 0;
// 取出双目图像(深拷贝)
MvsImageData leftImg, rightImg;
std::unique_ptr<unsigned char[]> leftBuf;
std::unique_ptr<unsigned char[]> rightBuf;
{
QMutexLocker lk(&m_frameMutex);
@ -3390,6 +3874,7 @@ void DroneScrewServerPresenter::detectThreadFunc()
haveProcessedFrame = true;
lastProcessedLeftFrameId = leftImg.frameID;
lastProcessedRightFrameId = rightImg.frameID;
}
// Publish the display frame before detection work. detectMode only controls raw transport.
if (m_bRawPubEnabled.load())
@ -3461,6 +3946,16 @@ void DroneScrewServerPresenter::detectThreadFunc()
const int pipelineMode = m_detectPipelineMode.load();
const bool distanceMode = (pipelineMode == kDetectPipelineDistance);
if (simulationMode && distanceMode &&
m_bLiveStreaming.load() && m_bRtspStarted.load())
{
const int rtspRet = pushRtspFrame(leftImg);
if (rtspRet != 0 && rtspRet != ERR_CODE(DEV_RESULT_EMPTY))
{
LOG_WARN("[SIM-LIVE] push frame failed ret=%d frame=%llu\n",
rtspRet, leftImg.frameID);
}
}
if (distanceMode)
{
if (m_pAlgo)
@ -3626,6 +4121,13 @@ void DroneScrewServerPresenter::detectThreadFunc()
LOG_DEBUG("[DETECT] frame #%d after detectionResult emit\n", frameCnt + 1);
frameCnt++;
if (simulationMode)
{
const int fps = std::max(1, m_activeTriggerFps.load());
const int periodMs =
std::max(1, static_cast<int>(1000.0 / static_cast<double>(fps)));
std::this_thread::sleep_for(std::chrono::milliseconds(periodMs));
}
if (frameCnt % 100 == 0)
{
// 打印最近检测到的目标摘要

View File

@ -65,6 +65,9 @@ public:
*/
bool saveConfiguration();
void setSimulationImageDir(const QString& dirPath);
bool isSimulationMode() const;
/**
* @brief 初始化相机 / 推流 / 算法
*/
@ -224,6 +227,12 @@ private:
void rightCameraCallback(const MvsImageData& img);
void detectThreadFunc();
int prepareSimulationImages(bool forceReload);
void clearSimulationImages();
bool loadNextSimulationImagePair(MvsImageData& leftImg,
MvsImageData& rightImg,
unsigned int targetWidth = 0,
unsigned int targetHeight = 0);
double exposureForRole(const char* role) const;
int applyExposureForRole(const char* role);
int applyCurrentExposureForRole(const char* role);
@ -279,6 +288,14 @@ private:
unsigned long long imageIndex{0};
};
struct SimulationImagePair
{
QString dirPath;
QString leftPath;
QString rightPath;
unsigned long long index{0};
};
// 双目相机设备
IMvsDevice* m_pLeftCamera{nullptr};
IMvsDevice* m_pRightCamera{nullptr};
@ -305,9 +322,9 @@ private:
double m_leftGain{1.0};
double m_rightGain{1.0};
QString m_configFilePath;
int m_zmqControlPort{5555};
int m_zmqResultPort{5556};
int m_zmqRawImagePort{5557};
int m_zmqControlPort{15555};
int m_zmqResultPort{15556};
int m_zmqRawImagePort{15557};
int m_rtspPort{8554};
QString m_rtspPath{"/live/dronescrew"};
QString m_rtspAdvertiseUrl;
@ -375,6 +392,12 @@ private:
std::vector<PrecisionResultSample> m_precisionResultSamples;
DroneScrewResult m_lastFinalResult;
bool m_hasLastFinalResult{false};
QString m_simulationImageDir;
std::vector<SimulationImagePair> m_simulationImagePairs;
size_t m_simulationImageCursor{0};
unsigned long long m_simulationFrameId{0};
mutable std::mutex m_simulationImageMutex;
};
#endif // DRONESCREW_SERVER_PRESENTER_H

View File

@ -479,9 +479,16 @@ QByteArray DroneScrewZmqProtocol::handleControlMessage(const QByteArray& reqData
else if (cmd == "stop")
{
emit rawImagePublishEnabledRequested(false);
const int streamRet = m_stopLiveStreamHandler ? m_stopLiveStreamHandler()
: 0;
const int ret = m_stopWorkHandler ? m_stopWorkHandler()
: fallbackHandlerError();
if (ret != 0)
resp["mode"] = "idle";
if (streamRet != 0)
{
setErrorResp(resp, streamRet, "stop live stream failed");
}
else if (ret != 0)
{
setErrorResp(resp, ret, "stop detection failed");
}

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 "5"
#define DRONESCREWSERVER_VERSION_BUILD "6"
#endif // DRONESCREW_VERSION_H

View File

@ -25,9 +25,9 @@
rawImagePort=0 表示不启用原始图像广播
原始图像订阅会发送左右目两张图像
-->
<Zmq controlPort="5555"
resultPort="5556"
rawImagePort="5557" />
<Zmq controlPort="15555"
resultPort="15556"
rawImagePort="15557" />
<!-- RTSP 拉流参数(客户端从 mediamtx 拉取 streamCamera 指定的相机图像)
port 是 RTSP 对外拉流端口,默认 8554;服务内部 RTMP 推流端口固定为 1935

View File

@ -4,6 +4,7 @@
#include <QFileInfo>
#include <QFile>
#include <QJsonObject>
#include <QStringList>
#include <iostream>
#include <chrono>
@ -316,6 +317,48 @@ int main(int argc, char* argv[])
DroneScrewServerPresenter presenter;
DroneScrewZmqProtocol zmq;
QString simulationImageDirArg;
bool simulationArgSpecified = false;
const QStringList args = QCoreApplication::arguments();
for (int i = 1; i < args.size(); ++i)
{
const QString arg = args.at(i);
if (arg == QStringLiteral("--simulate-dir") ||
arg == QStringLiteral("--sim-dir"))
{
simulationArgSpecified = true;
if (i + 1 >= args.size())
{
LOG_ERROR("[SIM] missing directory after %s\n",
arg.toStdString().c_str());
return ERR_CODE(FILE_ERR_NOEXIST);
}
simulationImageDirArg = args.at(++i);
continue;
}
const QString simulateDirPrefix = QStringLiteral("--simulate-dir=");
const QString simDirPrefix = QStringLiteral("--sim-dir=");
if (arg.startsWith(simulateDirPrefix))
{
simulationArgSpecified = true;
simulationImageDirArg = arg.mid(simulateDirPrefix.size());
continue;
}
if (arg.startsWith(simDirPrefix))
{
simulationArgSpecified = true;
simulationImageDirArg = arg.mid(simDirPrefix.size());
continue;
}
if (!arg.startsWith(QStringLiteral("-")) && !simulationArgSpecified)
{
simulationArgSpecified = true;
simulationImageDirArg = arg;
}
}
// 信号槽:协议 → presenter
QObject::connect(&zmq, &DroneScrewZmqProtocol::rawImagePublishEnabledRequested,
&presenter, &DroneScrewServerPresenter::setRawPubEnabled,
@ -421,6 +464,30 @@ int main(int argc, char* argv[])
}
// 初始化各子系统
if (simulationArgSpecified)
{
const QString trimmed = simulationImageDirArg.trimmed();
if (trimmed.isEmpty())
{
LOG_ERROR("[SIM] simulation image directory is empty\n");
return ERR_CODE(FILE_ERR_NOEXIST);
}
const QFileInfo inputInfo(trimmed);
const QString absolutePath = inputInfo.isAbsolute()
? inputInfo.absoluteFilePath()
: QDir::current().absoluteFilePath(trimmed);
const QFileInfo simulationDirInfo(absolutePath);
if (!simulationDirInfo.exists() || !simulationDirInfo.isDir())
{
LOG_ERROR("[SIM] simulation image directory does not exist: %s\n",
absolutePath.toStdString().c_str());
return ERR_CODE(FILE_ERR_NOEXIST);
}
presenter.setSimulationImageDir(simulationDirInfo.absoluteFilePath());
}
presenter.initAll();
// 启动 ZMQ