diff --git a/App/DroneScrewbolt/Doc/project_design.md b/App/DroneScrewbolt/Doc/project_design.md index 8ec41c04..d46b6f0c 100644 --- a/App/DroneScrewbolt/Doc/project_design.md +++ b/App/DroneScrewbolt/Doc/project_design.md @@ -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 拉取距离检测实时画面 | diff --git a/App/DroneScrewbolt/Doc/推流.png b/App/DroneScrewbolt/Doc/推流.png new file mode 100644 index 00000000..937a26d3 Binary files /dev/null and b/App/DroneScrewbolt/Doc/推流.png differ diff --git a/App/DroneScrewbolt/Doc/检测.png b/App/DroneScrewbolt/Doc/检测.png new file mode 100644 index 00000000..c53e63c5 Binary files /dev/null and b/App/DroneScrewbolt/Doc/检测.png differ diff --git a/App/DroneScrewbolt/DroneScrewCtrlApp/Version.h b/App/DroneScrewbolt/DroneScrewCtrlApp/Version.h index 677305f5..db0412d9 100644 --- a/App/DroneScrewbolt/DroneScrewCtrlApp/Version.h +++ b/App/DroneScrewbolt/DroneScrewCtrlApp/Version.h @@ -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; } diff --git a/App/DroneScrewbolt/DroneScrewCtrlApp/config/config.xml b/App/DroneScrewbolt/DroneScrewCtrlApp/config/config.xml index 364a1a68..cd003d80 100644 --- a/App/DroneScrewbolt/DroneScrewCtrlApp/config/config.xml +++ b/App/DroneScrewbolt/DroneScrewCtrlApp/config/config.xml @@ -4,9 +4,9 @@ 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) diff --git a/App/DroneScrewbolt/DroneScrewCtrlApp/mainwindow.h b/App/DroneScrewbolt/DroneScrewCtrlApp/mainwindow.h index 777bd8b9..798d9917 100644 --- a/App/DroneScrewbolt/DroneScrewCtrlApp/mainwindow.h +++ b/App/DroneScrewbolt/DroneScrewCtrlApp/mainwindow.h @@ -77,6 +77,7 @@ private: void setupSingleCameraLayout(); void setupMultiCameraLayout(); int getAvailableHeight(); + void updateVersionLabelAlignment(); private: Ui::MainWindow* ui{nullptr}; diff --git a/App/DroneScrewbolt/DroneScrewCtrlConfig/Inc/IVrConfig.h b/App/DroneScrewbolt/DroneScrewCtrlConfig/Inc/IVrConfig.h index f9ba3876..a6b88d4a 100644 --- a/App/DroneScrewbolt/DroneScrewCtrlConfig/Inc/IVrConfig.h +++ b/App/DroneScrewbolt/DroneScrewCtrlConfig/Inc/IVrConfig.h @@ -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; diff --git a/App/DroneScrewbolt/DroneScrewCtrlConfig/Src/VrConfig.cpp b/App/DroneScrewbolt/DroneScrewCtrlConfig/Src/VrConfig.cpp index 43e9346f..5acb5f2d 100644 --- a/App/DroneScrewbolt/DroneScrewCtrlConfig/Src/VrConfig.cpp +++ b/App/DroneScrewbolt/DroneScrewCtrlConfig/Src/VrConfig.cpp @@ -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); diff --git a/App/DroneScrewbolt/DroneScrewServer/DroneScrewServerPresenter.cpp b/App/DroneScrewbolt/DroneScrewServer/DroneScrewServerPresenter.cpp index 4ef1330a..026bc6f9 100644 --- a/App/DroneScrewbolt/DroneScrewServer/DroneScrewServerPresenter.cpp +++ b/App/DroneScrewbolt/DroneScrewServer/DroneScrewServerPresenter.cpp @@ -5,21 +5,25 @@ #include #include +#include #include #include #include #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include +#include #include #include #include @@ -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& 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(gray.width()) * static_cast(gray.height()); + if (imageSize > static_cast(std::numeric_limits::max())) + { + LOG_WARN("[SIM] image too large: %s size=%llu\n", + path.toStdString().c_str(), + static_cast(imageSize)); + return false; + } + + width = static_cast(gray.width()); + height = static_cast(gray.height()); + data.resize(static_cast(imageSize)); + for (int y = 0; y < gray.height(); ++y) + { + std::memcpy(data.data() + static_cast(y) * width, + gray.constScanLine(y), + width); + } + return true; +} + +bool resizeMono8Nearest(const std::vector& src, + unsigned int srcWidth, + unsigned int srcHeight, + unsigned int dstWidth, + unsigned int dstHeight, + std::vector& dst) +{ + if (srcWidth == 0 || srcHeight == 0 || dstWidth == 0 || dstHeight == 0) + return false; + + const uint64_t srcSize = + static_cast(srcWidth) * static_cast(srcHeight); + const uint64_t dstSize = + static_cast(dstWidth) * static_cast(dstHeight); + if (src.size() < srcSize || + dstSize > static_cast(std::numeric_limits::max())) + return false; + + dst.resize(static_cast(dstSize)); + for (unsigned int y = 0; y < dstHeight; ++y) + { + const unsigned int srcY = + static_cast((static_cast(y) * srcHeight) / dstHeight); + const unsigned char* srcRow = src.data() + static_cast(srcY) * srcWidth; + unsigned char* dstRow = dst.data() + static_cast(y) * dstWidth; + for (unsigned int x = 0; x < dstWidth; ++x) + { + const unsigned int srcX = + static_cast((static_cast(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 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 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 lk(m_simulationImageMutex); + const bool simulationMode = !m_simulationImageDir.trimmed().isEmpty(); + info["simulationMode"] = simulationMode; + info["simulationImageDir"] = m_simulationImageDir; + info["simulationImagePairs"] = static_cast(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(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(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(kPrecisionFrameRate); - return acqRet; + const int simRet = prepareSimulationImages(true); + if (simRet != 0) + { + emit statusChanged(QStringLiteral("仿真图像对加载失败")); + releaseRtspPusher(); + m_detectPipelineMode = kDetectPipelinePrecision; + m_activeTriggerFps = static_cast(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(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(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 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 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 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 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 leftData; + std::vector 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(targetWidth) * static_cast(targetHeight); + if (targetSize > static_cast(std::numeric_limits::max())) + { + LOG_WARN("[SIM] target image too large pair index=%llu target=%ux%u\n", + pair.index, targetWidth, targetHeight); + return false; + } + + std::vector resizedLeft; + std::vector 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::max() || + rightData.size() > std::numeric_limits::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 leftBuf(new (std::nothrow) unsigned char[leftData.size()]); + std::unique_ptr 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(QDateTime::currentMSecsSinceEpoch()) * 1000ull; + + leftImg = MvsImageData{}; + leftImg.pData = leftBuf.release(); + leftImg.width = leftWidth; + leftImg.height = leftHeight; + leftImg.dataSize = static_cast(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(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 leftBuf; + std::unique_ptr 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 leftBuf; - std::unique_ptr 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(1000.0 / static_cast(fps))); + std::this_thread::sleep_for(std::chrono::milliseconds(periodMs)); + } if (frameCnt % 100 == 0) { // 打印最近检测到的目标摘要 diff --git a/App/DroneScrewbolt/DroneScrewServer/DroneScrewServerPresenter.h b/App/DroneScrewbolt/DroneScrewServer/DroneScrewServerPresenter.h index 15e97051..4b526327 100644 --- a/App/DroneScrewbolt/DroneScrewServer/DroneScrewServerPresenter.h +++ b/App/DroneScrewbolt/DroneScrewServer/DroneScrewServerPresenter.h @@ -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 m_precisionResultSamples; DroneScrewResult m_lastFinalResult; bool m_hasLastFinalResult{false}; + + QString m_simulationImageDir; + std::vector m_simulationImagePairs; + size_t m_simulationImageCursor{0}; + unsigned long long m_simulationFrameId{0}; + mutable std::mutex m_simulationImageMutex; }; #endif // DRONESCREW_SERVER_PRESENTER_H diff --git a/App/DroneScrewbolt/DroneScrewServer/DroneScrewZmqProtocol.cpp b/App/DroneScrewbolt/DroneScrewServer/DroneScrewZmqProtocol.cpp index 5b2634f1..b88f0c1a 100644 --- a/App/DroneScrewbolt/DroneScrewServer/DroneScrewZmqProtocol.cpp +++ b/App/DroneScrewbolt/DroneScrewServer/DroneScrewZmqProtocol.cpp @@ -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"); } diff --git a/App/DroneScrewbolt/DroneScrewServer/Version.h b/App/DroneScrewbolt/DroneScrewServer/Version.h index d464c631..3b989b83 100644 --- a/App/DroneScrewbolt/DroneScrewServer/Version.h +++ b/App/DroneScrewbolt/DroneScrewServer/Version.h @@ -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 diff --git a/App/DroneScrewbolt/DroneScrewServer/config/config.xml b/App/DroneScrewbolt/DroneScrewServer/config/config.xml index a855c077..39ff2bf0 100644 --- a/App/DroneScrewbolt/DroneScrewServer/config/config.xml +++ b/App/DroneScrewbolt/DroneScrewServer/config/config.xml @@ -25,9 +25,9 @@ rawImagePort=0 表示不启用原始图像广播 原始图像订阅会发送左右目两张图像 --> - +