螺杆的本地仿真功能
This commit is contained in:
parent
0a15ce5adb
commit
2f3f6a61fc
@ -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 拉取距离检测实时画面 |
|
||||
|
||||
|
||||
BIN
App/DroneScrewbolt/Doc/推流.png
Normal file
BIN
App/DroneScrewbolt/Doc/推流.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
BIN
App/DroneScrewbolt/Doc/检测.png
Normal file
BIN
App/DroneScrewbolt/Doc/检测.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 798 KiB |
@ -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; }
|
||||
|
||||
|
||||
@ -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=""
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -77,6 +77,7 @@ private:
|
||||
void setupSingleCameraLayout();
|
||||
void setupMultiCameraLayout();
|
||||
int getAvailableHeight();
|
||||
void updateVersionLabelAlignment();
|
||||
|
||||
private:
|
||||
Ui::MainWindow* ui{nullptr};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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)
|
||||
{
|
||||
// 打印最近检测到的目标摘要
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user