#include "DroneScrewCtrlPresenter.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr int kDefaultRtspPullPort = 8554; constexpr int kLegacyRtmpPublishPort = 1935; constexpr unsigned int kRtspOpenTimeoutMs = 10000; constexpr int kRtspPullMaxAttempts = 20; constexpr int kRtspPullRetryDelayMs = 500; constexpr int kRtspFirstFrameWaitMs = 10000; constexpr int kControlStartTimeoutMs = 30000; constexpr int kControlStopTimeoutMs = 30000; constexpr size_t kMaxPendingMatchFrames = 30; constexpr qint64 kMaxPendingMatchAgeMs = 15000; bool hasReadableImageRows(const void* data, int width, int height, int stride, size_t size, unsigned int bytesPerPixel) { if (!data || width <= 0 || height <= 0 || stride <= 0) return false; const size_t minStride = static_cast(width) * bytesPerPixel; if (static_cast(stride) < minStride) return false; const size_t minSize = static_cast(stride) * static_cast(height); return size >= minSize; } bool hasReadableRows(const VrFFPulledFrame& frame, unsigned int bytesPerPixel) { return hasReadableImageRows(frame.data, static_cast(frame.width), static_cast(frame.height), static_cast(frame.stride), frame.size, bytesPerPixel); } QString configuredStreamUrl(const DroneScrewCtrlConfigResult& cfg) { const QString host = QString::fromStdString(cfg.server.serverIp).trimmed(); if (host.isEmpty()) return QString(); int port = cfg.server.rtspPort > 0 ? cfg.server.rtspPort : kDefaultRtspPullPort; if (port == kLegacyRtmpPublishPort) port = kDefaultRtspPullPort; QString path = QString::fromStdString(cfg.server.rtspPath).trimmed(); if (path.isEmpty()) path = QStringLiteral("/live/dronescrew"); if (!path.startsWith('/')) path.prepend('/'); QString auth; const QString user = QString::fromStdString(cfg.server.rtspUser); const QString pass = QString::fromStdString(cfg.server.rtspPass); if (!user.isEmpty()) { auth = user; if (!pass.isEmpty()) auth += QStringLiteral(":") + pass; auth += QStringLiteral("@"); } return QStringLiteral("rtsp://%1%2:%3%4").arg(auth).arg(host).arg(port).arg(path); } QString normalizeStreamUrl(QString url, const DroneScrewCtrlConfigResult& cfg) { url = url.trimmed(); if (url.isEmpty()) return configuredStreamUrl(cfg); QUrl parsed(url); if (parsed.isValid() && !parsed.scheme().isEmpty()) { const QString scheme = parsed.scheme().toLower(); if (scheme == QStringLiteral("rtsp") || scheme == QStringLiteral("rtmp")) { parsed.setScheme(QStringLiteral("rtsp")); const QString configuredHost = QString::fromStdString(cfg.server.serverIp).trimmed(); const QString parsedHost = parsed.host().trimmed(); if (!configuredHost.isEmpty() && (parsedHost.isEmpty() || parsedHost == QStringLiteral("0.0.0.0") || parsedHost == QStringLiteral("127.0.0.1") || parsedHost.compare(QStringLiteral("localhost"), Qt::CaseInsensitive) == 0)) { parsed.setHost(configuredHost); } const int configuredPort = (cfg.server.rtspPort > 0 && cfg.server.rtspPort != kLegacyRtmpPublishPort) ? cfg.server.rtspPort : kDefaultRtspPullPort; if (parsed.port(-1) <= 0 || parsed.port(-1) == kLegacyRtmpPublishPort) parsed.setPort(configuredPort); return parsed.toString(QUrl::FullyEncoded); } } if (url.startsWith(QStringLiteral("rtmp://"), Qt::CaseInsensitive)) url.replace(0, 4, QStringLiteral("rtsp")); url.replace(QStringLiteral(":1935/"), QStringLiteral(":8554/")); return url; } QSize overlayCoordinateSize(const CtrlDetectionFrame& frame, const QImage& img, const QSize& sourceSize) { if (sourceSize.width() > 0 && sourceSize.height() > 0) return sourceSize; if (frame.imageWidth > 0 && frame.imageHeight > 0) return QSize(frame.imageWidth, frame.imageHeight); return img.size(); } qint64 nowMs() { return QDateTime::currentMSecsSinceEpoch(); } void processEventsForMs(int waitMs) { if (waitMs <= 0) return; QElapsedTimer timer; timer.start(); while (timer.elapsed() < waitMs) { QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 20); QThread::msleep(10); } } } DroneScrewCtrlPresenter::DroneScrewCtrlPresenter(QObject* parent) : QObject(parent) { m_pReconnectTimer = new QTimer(this); m_pReconnectTimer->setInterval(3000); connect(m_pReconnectTimer, &QTimer::timeout, this, &DroneScrewCtrlPresenter::onReconnectTimer); connect(this, &DroneScrewCtrlPresenter::displayFrameReady, this, &DroneScrewCtrlPresenter::onOverlayFrameReady, Qt::QueuedConnection); connect(this, &DroneScrewCtrlPresenter::detectionResultReady, this, &DroneScrewCtrlPresenter::onResultReceived, Qt::QueuedConnection); } DroneScrewCtrlPresenter::~DroneScrewCtrlPresenter() { DeinitApp(); } void DroneScrewCtrlPresenter::ApplyConfig(const DroneScrewCtrlConfigResult& cfg) { m_cfg = cfg; if (m_bRunning.load()) { DeinitApp(); InitApp(); } } void DroneScrewCtrlPresenter::UpdateDisplayOptions(const DroneScrewDisplayOption& options) { m_cfg.display = options; } int DroneScrewCtrlPresenter::InitApp() { if (m_bRunning.load()) return 0; // 1) 创建 WDRemoteReceiver 并注入回调(控制 + 结果 + 原始图) if (IWDRemoteReceiver::CreateInstance(&m_pRemote) != 0 || !m_pRemote) { m_bConnected = false; emit serverConnectionChanged(false); m_pReconnectTimer->start(); return -1; } m_pRemote->SetEventCallback( [this](WDRemoteEventType ev, const std::string& msg) { this->onRemoteEvent(ev, msg); }); m_pRemote->SetDetectionCallback( [this](const WDRemoteDetectionFrame& f) { this->onRemoteDetection(f); }); // 检测模式下板一关闭 RTSP、改用 ZMQ 原始图通道发图;订阅之以显示检测结果图像 m_pRemote->SetRawImageCallback( [this](const WDRemoteBinocularRawImage& img) { this->onRemoteRawImage(img); }); // 2) UDP 搜索设备 → 默认自动打开第一个;搜不到不再按固定 IP 直接交互。 bool opened = false; std::vector devices; if (m_pRemote->SearchDevices(devices, 500) == 0 && !devices.empty()) { const WDRemoteDeviceInfo& d = devices.front(); if (m_pRemote->Open(d.machineCode) == 0) { // 用发现到的设备信息更新连接参数,供 RTSP 拉流使用 if (!d.serverIp.empty()) m_cfg.server.serverIp = d.serverIp; if (d.controlPort > 0) m_cfg.server.zmqControlPort = d.controlPort; if (d.resultPort > 0) m_cfg.server.zmqResultPort = d.resultPort; m_cfg.server.zmqRawImagePort = d.rawImagePort; m_streamUrl = resolveStreamUrl(QString::fromStdString(d.rtspUrl)); opened = true; emit statusMessage(QStringLiteral("已自动打开设备 %1 (%2)") .arg(QString::fromStdString(d.deviceName), QString::fromStdString(d.serverIp))); } } if (!opened) { emit statusMessage(QStringLiteral("未发现 DroneScrewServer,等待自动搜索")); m_bConnected = false; emit serverConnectionChanged(false); delete m_pRemote; m_pRemote = nullptr; m_pReconnectTimer->start(); return -1; } // 拉流器等 start_stream 成功后再打开;地址以 Server get_info/discovery 返回为准。 m_bConnected = true; m_bRunning = true; emit serverConnectionChanged(true); emit statusMessage(QStringLiteral("已连接到服务器")); // 4) 延迟从 Server 获取参数(相机参数、算法参数)并更新 UI QTimer::singleShot(200, this, [this]() { if (m_pRemote && m_bConnected.load()) { WDRemoteServerInfo info = m_pRemote->GetServerInfo(); if (info.ok) { updateStreamInfo(info); emit serverInfoReceived(info); } } }); return 0; } void DroneScrewCtrlPresenter::DeinitApp() { if (m_pReconnectTimer && m_pReconnectTimer->isActive()) m_pReconnectTimer->stop(); m_liveStreamMode = false; releasePuller(); if (m_pRemote) { m_pRemote->Disconnect(); delete m_pRemote; m_pRemote = nullptr; } m_bRunning = false; m_bConnected = false; m_streamUrl.clear(); } void DroneScrewCtrlPresenter::onReconnectTimer() { if (m_bRunning.load() && m_bConnected.load()) { m_pReconnectTimer->stop(); return; } emit statusMessage(QStringLiteral("尝试重新连接...")); DeinitApp(); if (InitApp() == 0) m_pReconnectTimer->stop(); } void DroneScrewCtrlPresenter::updateStreamInfo(const WDRemoteServerInfo& info) { if (!info.ok) return; m_streamUrl = resolveStreamUrl(QString::fromStdString(info.rtspUrl)); } void DroneScrewCtrlPresenter::releasePuller() { m_rtspReceiving = false; m_rtspFirstFrameReported = false; if (!m_pPuller) return; IVrFFMediaPuller* puller = m_pPuller; m_pPuller = nullptr; puller->SetFrameCallback(FFPulledFrameCallback{}); puller->Stop(); puller->UnInit(); delete puller; } QString DroneScrewCtrlPresenter::resolveStreamUrl(const QString& streamUrl) const { return normalizeStreamUrl(streamUrl, m_cfg); } bool DroneScrewCtrlPresenter::initPuller(const QString& streamUrl, bool quiet) { releasePuller(); const QString url = resolveStreamUrl(streamUrl); if (url.isEmpty()) { if (!quiet) emit statusMessage(QStringLiteral("Server 未返回拉流地址")); return false; } if (!IVrFFMediaPuller::CreateObject(&m_pPuller) || !m_pPuller) { if (!quiet) emit statusMessage(QStringLiteral("创建拉流器失败")); return false; } VrFFPullConfig pc; pc.rtspUrl = url.toStdString(); pc.useTcp = m_cfg.server.rtspUseTcp; pc.networkTimeoutMs = kRtspOpenTimeoutMs; pc.outputFormat = EFFPullPixelFormat::NV12; m_pPuller->SetFrameCallback([this](const VrFFPulledFrame& f) { if (!m_rtspReceiving.load() || !m_bRunning.load()) return; this->onRtspFrame(f); }); const int ret = m_pPuller->Init(pc); if (ret != 0) { if (!quiet) emit statusMessage(QStringLiteral("打开拉流地址失败 ret=%1: %2").arg(ret).arg(url)); releasePuller(); return false; } return true; } bool DroneScrewCtrlPresenter::startPullerOnce(const QString& streamUrl, bool quiet) { const QString url = resolveStreamUrl(streamUrl); if (url.isEmpty()) { if (!quiet) emit statusMessage(QStringLiteral("Server 未返回拉流地址,配置也无法生成地址")); return false; } m_streamUrl = url; if (!quiet) { emit statusMessage(QStringLiteral("正在打开拉流: %1").arg(url)); QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 20); } if (initPuller(url, quiet)) { m_rtspFirstFrameReported = false; m_rtspReceiving = true; const int startRet = m_pPuller ? m_pPuller->Start() : -1; if (startRet == 0) { m_streamUrl = url; if (!quiet) emit statusMessage(QStringLiteral("拉流器已启动: %1").arg(url)); return true; } m_rtspReceiving = false; if (!quiet) emit statusMessage(QStringLiteral("启动拉流器失败 ret=%1: %2").arg(startRet).arg(url)); releasePuller(); } if (!quiet) emit statusMessage(QStringLiteral("启动拉流失败: %1").arg(url)); return false; } bool DroneScrewCtrlPresenter::waitForRtspFirstFrame(int timeoutMs) { QElapsedTimer timer; timer.start(); while (timer.elapsed() < timeoutMs) { if (m_rtspFirstFrameReported.load()) return true; if (!m_rtspReceiving.load()) return false; QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 20); QThread::msleep(10); } return m_rtspFirstFrameReported.load(); } bool DroneScrewCtrlPresenter::startPullerWithRetry(const QString& streamUrl) { const QString url = resolveStreamUrl(streamUrl); if (url.isEmpty()) { emit statusMessage(QStringLiteral("Server 未返回拉流地址,配置也无法生成地址")); return false; } m_streamUrl = url; emit statusMessage(QStringLiteral("正在拉流: %1").arg(url)); QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 20); for (int attempt = 1; attempt <= kRtspPullMaxAttempts; ++attempt) { const bool started = startPullerOnce(url, true); if (started && waitForRtspFirstFrame(kRtspFirstFrameWaitMs)) return true; releasePuller(); if (attempt < kRtspPullMaxAttempts) { processEventsForMs(kRtspPullRetryDelayMs); } } emit statusMessage(QStringLiteral("本地 RTSP 拉流未收到首帧,Server 推流保持运行: %1").arg(url)); return false; } bool DroneScrewCtrlPresenter::TriggerSingleDetection() { return m_pRemote && m_pRemote->RequestSingleDetection() == 0; } bool DroneScrewCtrlPresenter::StartLiveStream() { if (!m_pRemote) return false; releasePuller(); clearCachedDisplayState(); m_liveStreamMode = false; emit statusMessage(QStringLiteral("正在请求服务端启动距离检测...")); QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 20); const int startRet = m_pRemote->StartWork(WDRemoteWorkMode::LiveStream, kControlStartTimeoutMs); bool ok = (startRet == 0); if (ok) { m_liveStreamMode = true; emit statusMessage(QStringLiteral("服务端已启动,正在获取拉流地址...")); QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 20); WDRemoteServerInfo info = m_pRemote->GetServerInfo(); if (info.ok) { updateStreamInfo(info); emit serverInfoReceived(info); } const bool pullOk = startPullerWithRetry(m_streamUrl); if (!pullOk) { releasePuller(); emit statusMessage(QStringLiteral("Server 已开流,本地暂未拉到图像: %1").arg(m_streamUrl)); return true; } emit statusMessage(QStringLiteral("Server 已开流,RTSP 已出图: %1").arg(m_streamUrl)); } if (!ok) { emit statusMessage(QStringLiteral("Server 启动距离检测失败 ret=%1").arg(startRet)); m_liveStreamMode = false; } return ok; } bool DroneScrewCtrlPresenter::StopLiveStream() { if (!m_pRemote) return false; m_liveStreamMode = false; // 停止 RTSP 拉流 releasePuller(); const int stopRet = m_pRemote->StopWork(WDRemoteWorkMode::LiveStream, kControlStopTimeoutMs); bool ok = (stopRet == 0); if (ok) emit statusMessage(QStringLiteral("Server 已关流")); else emit statusMessage(QStringLiteral("Server 关流失败 ret=%1").arg(stopRet)); return ok; } bool DroneScrewCtrlPresenter::StartDetection() { if (!m_pRemote) return false; m_liveStreamMode = false; // 检测模式下停止 RTSP 拉流(改用 ZMQ 原始图像通道) releasePuller(); clearCachedDisplayState(); // 使用单目传输模式:仅接收左目图像,减少网络带宽消耗 m_pRemote->SetDetectMode("mono"); const int startRet = m_pRemote->StartWork(WDRemoteWorkMode::Detection, kControlStartTimeoutMs); bool ok = (startRet == 0); if (ok) emit statusMessage(QStringLiteral("Server 已开始检测(单目传输)")); else emit statusMessage(QStringLiteral("Server 启动精准检测失败 ret=%1").arg(startRet)); return ok; } bool DroneScrewCtrlPresenter::StopDetection() { if (!m_pRemote) return false; const int stopRet = m_pRemote->StopWork(WDRemoteWorkMode::Detection, kControlStopTimeoutMs); bool ok = (stopRet == 0); if (ok) emit statusMessage(QStringLiteral("Server 已停止检测")); else emit statusMessage(QStringLiteral("Server 停止检测失败 ret=%1").arg(stopRet)); return ok; } bool DroneScrewCtrlPresenter::StopAll() { if (!m_pRemote) return false; m_liveStreamMode = false; // 停止 RTSP 拉流 releasePuller(); clearCachedDisplayState(); // 两种模式都停掉,保证服务端回到空闲(幂等) const int streamRet = m_pRemote->StopWork(WDRemoteWorkMode::LiveStream, kControlStopTimeoutMs); const int detectRet = m_pRemote->StopWork(WDRemoteWorkMode::Detection, kControlStopTimeoutMs); const bool ok = (streamRet == 0 && detectRet == 0); if (ok) emit statusMessage(QStringLiteral("Server 已停止(实时/检测)")); else emit statusMessage(QStringLiteral("Server 停止失败 streamRet=%1 detectRet=%2") .arg(streamRet) .arg(detectRet)); return ok; } bool DroneScrewCtrlPresenter::SetServerExposure(double exposureTime) { return m_pRemote && m_pRemote->SetExposure(exposureTime) == 0; } bool DroneScrewCtrlPresenter::SetServerGain(double gain) { return m_pRemote && m_pRemote->SetGain(gain) == 0; } bool DroneScrewCtrlPresenter::PushAlgoParams(const DroneScrewAlgoUiParams& params) { if (!m_pRemote) return false; WDRemoteAlgoParams p; p.scoreThreshold = params.scoreThreshold; p.nmsThreshold = params.nmsThreshold; p.inputWidth = params.inputWidth; p.inputHeight = params.inputHeight; p.modelType = params.modelType; p.expectedBoltCount = params.expectedBoltCount; return m_pRemote->SetAlgoParams(p) == 0; } bool DroneScrewCtrlPresenter::QueryServerInfo(WDRemoteServerInfo& info, int timeoutMs) { info = WDRemoteServerInfo{}; if (!m_pRemote || !m_bConnected.load()) return false; info = m_pRemote->GetServerInfo(timeoutMs); if (!info.ok) return false; updateStreamInfo(info); return true; } // ===================================================================== // WDRemoteReceiver 回调 // ===================================================================== void DroneScrewCtrlPresenter::onRemoteDetection(const WDRemoteDetectionFrame& f) { CtrlDetectionFrame frame = toCtrlFrame(f); if (m_liveStreamMode.load() || m_rtspReceiving.load()) { { QMutexLocker lk(&m_lastResultMutex); m_lastResult = frame; m_bHasResult = true; } emit detectionResultReady(frame); return; } DisplayFrame matchedFrame; bool hasMatchedImage = false; { QMutexLocker lk(&m_lastImgMutex); auto rawIt = m_pendingRawFrames.find(frame.frameId); if (rawIt != m_pendingRawFrames.end()) { matchedFrame = rawIt->second.frame; m_pendingRawFrames.erase(rawIt); prunePendingMatchesLocked(); hasMatchedImage = true; } else { m_pendingDetectionResults[frame.frameId] = PendingDetectionResult{frame, nowMs()}; prunePendingMatchesLocked(); } } if (!hasMatchedImage) { { QMutexLocker lk(&m_lastResultMutex); m_lastResult = frame; m_bHasResult = true; } emit detectionResultReady(frame); return; } { QMutexLocker lk(&m_lastResultMutex); m_lastResult = frame; m_bHasResult = true; } emit detectionResultReady(frame); emit displayFrameReady(matchedFrame.image, matchedFrame.sourceSize, frame.frameId, false, frame, true); } void DroneScrewCtrlPresenter::onRemoteRawImage(const WDRemoteBinocularRawImage& img) { // 检测模式下板一通过 ZMQ 原始图通道发来左目图像(此时 RTSP 已关闭)。 // 转为 QImage,走与 RTSP 相同的叠加+显示通路(onOverlayFrameReady 会叠加检测框)。 if (!img.leftData || img.leftWidth <= 0 || img.leftHeight <= 0) return; if (m_liveStreamMode.load() || m_rtspReceiving.load()) return; QImage q; switch (img.leftPixelFormat) { case WDRemotePixelFormat::MONO8: if (!hasReadableImageRows(img.leftData, img.leftWidth, img.leftHeight, img.leftStride, img.leftDataSize, 1)) return; q = QImage(reinterpret_cast(img.leftData), img.leftWidth, img.leftHeight, img.leftStride, QImage::Format_Grayscale8).copy(); break; case WDRemotePixelFormat::RGB8: if (!hasReadableImageRows(img.leftData, img.leftWidth, img.leftHeight, img.leftStride, img.leftDataSize, 3)) return; q = QImage(reinterpret_cast(img.leftData), img.leftWidth, img.leftHeight, img.leftStride, QImage::Format_RGB888).copy(); break; case WDRemotePixelFormat::BGR8: if (!hasReadableImageRows(img.leftData, img.leftWidth, img.leftHeight, img.leftStride, img.leftDataSize, 3)) return; q = QImage(reinterpret_cast(img.leftData), img.leftWidth, img.leftHeight, img.leftStride, QImage::Format_RGB888).rgbSwapped(); break; default: return; } if (!q.isNull()) { const qint64 frameId = static_cast(img.frameId); const QSize sourceSize(img.leftSourceWidth, img.leftSourceHeight); const DisplayFrame displayFrame{q, sourceSize}; CtrlDetectionFrame matchedResult; bool hasMatchedResult = false; { QMutexLocker lk(&m_lastImgMutex); auto resultIt = m_pendingDetectionResults.find(frameId); if (resultIt != m_pendingDetectionResults.end()) { matchedResult = resultIt->second.result; m_pendingDetectionResults.erase(resultIt); prunePendingMatchesLocked(); hasMatchedResult = true; } else { m_pendingRawFrames[frameId] = PendingRawFrame{displayFrame, nowMs()}; prunePendingMatchesLocked(); } } if (!hasMatchedResult) { return; } { QMutexLocker lk(&m_lastResultMutex); m_lastResult = matchedResult; m_bHasResult = true; } emit detectionResultReady(matchedResult); emit displayFrameReady(q, sourceSize, frameId, false, matchedResult, true); } } void DroneScrewCtrlPresenter::onRemoteEvent(WDRemoteEventType ev, const std::string& msg) { switch (ev) { case WDRemoteEventType::CONNECTED: m_bConnected = true; emit serverConnectionChanged(true); emit statusMessage(QStringLiteral("控制通道已连接")); break; case WDRemoteEventType::DISCONNECTED: m_bConnected = false; emit serverConnectionChanged(false); emit statusMessage(QStringLiteral("控制通道已断开")); if (m_bRunning.load()) m_pReconnectTimer->start(); break; case WDRemoteEventType::CONNECTION_ERROR: m_bConnected = false; emit serverConnectionChanged(false); emit statusMessage(QStringLiteral("连接错误: %1").arg(QString::fromStdString(msg))); if (m_bRunning.load()) m_pReconnectTimer->start(); break; case WDRemoteEventType::REQUEST_TIMEOUT: emit statusMessage(QStringLiteral("请求超时")); break; default: break; } } CtrlDetectionFrame DroneScrewCtrlPresenter::toCtrlFrame(const WDRemoteDetectionFrame& f) { CtrlDetectionFrame frame; frame.frameId = static_cast(f.frameId); frame.timestampUs = f.timestampUs; frame.imageWidth = f.imageWidth; frame.imageHeight = f.imageHeight; frame.success = f.success; frame.errorCode = f.errorCode; frame.message = QString::fromStdString(f.message); frame.isFinalResult = f.isFinalResult; frame.rankScore = f.rankScore; frame.rankSampleCount = f.rankSampleCount; frame.saveIndex = static_cast(f.saveIndex); frame.boxes.reserve(f.boxes.size()); for (const auto& b : f.boxes) { CtrlDetectionBox cb; cb.classId = b.classId; cb.score = b.score; cb.x = b.x; cb.y = b.y; cb.width = b.width; cb.height = b.height; cb.hasPhysicalHeight = b.hasPhysicalHeight; cb.physicalHeightMm = b.physicalHeightMm; cb.hasStereoMatch = b.hasStereoMatch; cb.trusted = b.trusted; cb.matchConfidence = b.matchConfidence; cb.matchStatus = QString::fromStdString(b.matchStatus); cb.pairId = b.pairId; cb.leftIndex = b.leftIndex; cb.rightIndex = b.rightIndex; frame.boxes.push_back(cb); } frame.distances.reserve(f.distances.size()); for (const auto& d : f.distances) { CtrlDetectionDistance cd; cd.fromId = d.fromId; cd.toId = d.toId; cd.distanceMm = d.distanceMm; frame.distances.push_back(cd); } return frame; } // ===================================================================== // RTSP // ===================================================================== void DroneScrewCtrlPresenter::onRtspFrame(const VrFFPulledFrame& frame) { if (!m_rtspReceiving.load()) return; QImage img; if (frame.format == EFFPullPixelFormat::NV12) { if (!hasReadableRows(frame, 1)) return; img = QImage(static_cast(frame.data), frame.width, frame.height, frame.stride, QImage::Format_Grayscale8).copy(); } else if (frame.format == EFFPullPixelFormat::RGBA32) { if (!hasReadableRows(frame, 4)) return; img = QImage(static_cast(frame.data), frame.width, frame.height, frame.stride, QImage::Format_RGBA8888).copy(); } else if (frame.format == EFFPullPixelFormat::BGR24) { if (!hasReadableRows(frame, 3)) return; img = QImage(static_cast(frame.data), frame.width, frame.height, frame.stride, QImage::Format_BGR888).copy(); } else { return; } CtrlDetectionFrame latestResult; bool hasLatestResult = false; if (m_liveStreamMode.load()) { QMutexLocker lk(&m_lastResultMutex); if (m_bHasResult) { latestResult = m_lastResult; hasLatestResult = true; } } if (!m_rtspFirstFrameReported.exchange(true)) emit statusMessage(QStringLiteral("RTSP 已出图")); emit displayFrameReady(img, img.size(), -1, true, latestResult, hasLatestResult); } void DroneScrewCtrlPresenter::onOverlayFrameReady(const QImage& img, const QSize& sourceSize, qint64 frameId, bool rtspFrame, const CtrlDetectionFrame& result, bool hasResult) { QImage frame = img; if (hasResult && !m_liveStreamMode.load()) { QSize overlaySourceSize = sourceSize; if (rtspFrame) { overlaySourceSize = (result.imageWidth > 0 && result.imageHeight > 0) ? QSize(result.imageWidth, result.imageHeight) : img.size(); } overlayDetectionBoxes(frame, result, overlaySourceSize); } { QMutexLocker lk(&m_lastImgMutex); m_lastSourceImage = img; m_lastSourceFrameId = frameId; m_lastSourceIsRtsp = rtspFrame; m_lastDisplay = frame; } if (m_pReceiver) m_pReceiver->OnRtspFrame(frame); } void DroneScrewCtrlPresenter::onResultReceived(const CtrlDetectionFrame& result) { if (m_pReceiver) m_pReceiver->OnDetectionResult(result); } void DroneScrewCtrlPresenter::clearCachedDisplayState() { { QMutexLocker lk(&m_lastResultMutex); m_bHasResult = false; m_lastResult = CtrlDetectionFrame{}; } { QMutexLocker lk(&m_lastImgMutex); m_lastSourceImage = QImage(); m_lastSourceFrameId = -1; m_lastSourceIsRtsp = false; m_lastDisplay = QImage(); m_pendingRawFrames.clear(); m_pendingDetectionResults.clear(); } } void DroneScrewCtrlPresenter::prunePendingMatchesLocked() { const qint64 expireBefore = nowMs() - kMaxPendingMatchAgeMs; for (auto it = m_pendingRawFrames.begin(); it != m_pendingRawFrames.end(); ) { if (it->second.receivedAtMs < expireBefore) it = m_pendingRawFrames.erase(it); else ++it; } for (auto it = m_pendingDetectionResults.begin(); it != m_pendingDetectionResults.end(); ) { if (it->second.receivedAtMs < expireBefore) it = m_pendingDetectionResults.erase(it); else ++it; } while (m_pendingRawFrames.size() > kMaxPendingMatchFrames) m_pendingRawFrames.erase(m_pendingRawFrames.begin()); while (m_pendingDetectionResults.size() > kMaxPendingMatchFrames) m_pendingDetectionResults.erase(m_pendingDetectionResults.begin()); } void DroneScrewCtrlPresenter::overlayDetectionBoxes(QImage& img, const CtrlDetectionFrame& frame, const QSize& sourceSize) { if (!m_cfg.display.drawBoxes || frame.boxes.empty()) return; QPainter p(&img); p.setRenderHint(QPainter::Antialiasing, false); const int thickness = std::max(2, m_cfg.display.boxThickness); QPen pen(QColor(0, 0, 0), thickness); pen.setJoinStyle(Qt::MiterJoin); p.setPen(pen); QFont f = p.font(); f.setPointSize(28); f.setBold(true); p.setFont(f); const QFontMetrics fm(f); const QSize coordSize = overlayCoordinateSize(frame, img, sourceSize); const int coordWidth = coordSize.width(); const int coordHeight = coordSize.height(); const double scaleX = (coordWidth > 0) ? static_cast(img.width()) / coordWidth : 1.0; const double scaleY = (coordHeight > 0) ? static_cast(img.height()) / coordHeight : 1.0; const QRect imageRect(0, 0, img.width(), img.height()); for (size_t i = 0; i < frame.boxes.size(); ++i) { const auto& b = frame.boxes[i]; if (b.width <= 0 || b.height <= 0) continue; const int x1 = qRound(static_cast(b.x) * scaleX); const int y1 = qRound(static_cast(b.y) * scaleY); int x2 = qRound(static_cast(b.x + b.width) * scaleX) - 1; int y2 = qRound(static_cast(b.y + b.height) * scaleY) - 1; if (x2 < x1) x2 = x1; if (y2 < y1) y2 = y1; QRect r(QPoint(x1, y1), QPoint(x2, y2)); r = r.normalized().intersected(imageRect); if (r.isEmpty()) continue; p.drawRect(r); const QString label = QString::number(static_cast(i + 1)); const QPoint labelPos(r.right() + 4, r.top() + fm.ascent()); p.drawText(labelPos, label); } } QImage DroneScrewCtrlPresenter::GetLastDisplayImage() const { QMutexLocker lk(&m_lastImgMutex); return m_lastDisplay; }