887 lines
29 KiB
C++
Raw Normal View History

2026-06-26 17:55:15 +08:00
#include "DroneScrewCtrlPresenter.h"
#include <QPainter>
#include <QFont>
#include <QFontMetrics>
#include <QCoreApplication>
#include <QDateTime>
#include <QEventLoop>
#include <QElapsedTimer>
#include <QSize>
#include <QThread>
#include <QUrl>
#include <QtGlobal>
namespace
{
constexpr int kDefaultRtspPullPort = 8554;
constexpr int kLegacyRtmpPublishPort = 1935;
constexpr unsigned int kRtspOpenTimeoutMs = 5000;
constexpr int kRtspPullMaxAttempts = 5;
constexpr int kRtspPullRetryDelayMs = 500;
constexpr int kRtspFirstFrameWaitMs = 5000;
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<size_t>(width) * bytesPerPixel;
if (static_cast<size_t>(stride) < minStride)
return false;
const size_t minSize = static_cast<size_t>(stride) * static_cast<size_t>(height);
return size >= minSize;
}
bool hasReadableRows(const VrFFPulledFrame& frame, unsigned int bytesPerPixel)
{
return hasReadableImageRows(frame.data,
static_cast<int>(frame.width),
static_cast<int>(frame.height),
static_cast<int>(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;
return QSize(frame.imageWidth > 0 ? frame.imageWidth : img.width(),
frame.imageHeight > 0 ? frame.imageHeight : img.height());
}
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);
}
}
}
2026-06-26 17:55:15 +08:00
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,
2026-06-26 17:55:15 +08:00
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();
}
}
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<WDRemoteDeviceInfo> 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));
2026-06-26 17:55:15 +08:00
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();
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));
2026-06-26 17:55:15 +08:00
}
void DroneScrewCtrlPresenter::releasePuller()
{
m_rtspReceiving = false;
m_rtspFirstFrameReported = false;
2026-06-26 17:55:15 +08:00
if (!m_pPuller) return;
IVrFFMediaPuller* puller = m_pPuller;
2026-06-26 17:55:15 +08:00
m_pPuller = nullptr;
puller->SetFrameCallback(FFPulledFrameCallback{});
puller->Stop();
puller->UnInit();
delete puller;
}
QString DroneScrewCtrlPresenter::resolveStreamUrl(const QString& streamUrl) const
{
return normalizeStreamUrl(streamUrl, m_cfg);
2026-06-26 17:55:15 +08:00
}
bool DroneScrewCtrlPresenter::initPuller(const QString& streamUrl)
{
releasePuller();
const QString url = resolveStreamUrl(streamUrl);
2026-06-26 17:55:15 +08:00
if (url.isEmpty())
{
emit statusMessage(QStringLiteral("Server 未返回拉流地址"));
return false;
}
if (!IVrFFMediaPuller::CreateObject(&m_pPuller) || !m_pPuller)
{
emit statusMessage(QStringLiteral("创建拉流器失败"));
return false;
}
VrFFPullConfig pc;
pc.rtspUrl = url.toStdString();
pc.useTcp = m_cfg.server.rtspUseTcp;
pc.networkTimeoutMs = kRtspOpenTimeoutMs;
2026-06-26 17:55:15 +08:00
pc.outputFormat = EFFPullPixelFormat::NV12;
m_pPuller->SetFrameCallback([this](const VrFFPulledFrame& f) {
if (!m_rtspReceiving.load() || !m_bRunning.load())
return;
2026-06-26 17:55:15 +08:00
this->onRtspFrame(f);
});
const int ret = m_pPuller->Init(pc);
if (ret != 0)
{
emit statusMessage(QStringLiteral("打开拉流地址失败 ret=%1: %2").arg(ret).arg(url));
2026-06-26 17:55:15 +08:00
releasePuller();
return false;
}
return true;
}
bool DroneScrewCtrlPresenter::startPullerOnce(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);
if (initPuller(url))
{
m_rtspFirstFrameReported = false;
m_rtspReceiving = true;
const int startRet = m_pPuller ? m_pPuller->Start() : -1;
if (startRet == 0)
{
m_streamUrl = url;
emit statusMessage(QStringLiteral("拉流器已启动: %1").arg(url));
return true;
}
m_rtspReceiving = false;
emit statusMessage(QStringLiteral("启动拉流器失败 ret=%1: %2").arg(startRet).arg(url));
releasePuller();
}
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;
for (int attempt = 1; attempt <= kRtspPullMaxAttempts; ++attempt)
{
emit statusMessage(QStringLiteral("正在拉流(%1/%2): %3")
.arg(attempt)
.arg(kRtspPullMaxAttempts)
.arg(url));
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 20);
if (startPullerOnce(url) && waitForRtspFirstFrame(kRtspFirstFrameWaitMs))
return true;
emit statusMessage(QStringLiteral("本地 RTSP 未收到首帧(%1/%2): %3")
.arg(attempt)
.arg(kRtspPullMaxAttempts)
.arg(url));
releasePuller();
if (attempt < kRtspPullMaxAttempts)
{
emit statusMessage(QStringLiteral("本地 RTSP 拉流失败,准备重试(%1/%2): %3")
.arg(attempt + 1)
.arg(kRtspPullMaxAttempts)
.arg(url));
processEventsForMs(kRtspPullRetryDelayMs);
}
}
return false;
}
2026-06-26 17:55:15 +08:00
bool DroneScrewCtrlPresenter::TriggerSingleDetection()
{
return m_pRemote && m_pRemote->RequestSingleDetection() == 0;
}
bool DroneScrewCtrlPresenter::StartLiveStream()
{
if (!m_pRemote) return false;
releasePuller();
clearCachedDisplayState();
2026-06-26 17:55:15 +08:00
m_pRemote->StopWork(WDRemoteWorkMode::Detection);
emit statusMessage(QStringLiteral("正在请求服务端启动距离检测..."));
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 20);
2026-06-26 17:55:15 +08:00
bool ok = m_pRemote->StartWork(WDRemoteWorkMode::LiveStream) == 0;
if (ok)
{
emit statusMessage(QStringLiteral("服务端已启动,正在获取拉流地址..."));
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 20);
2026-06-26 17:55:15 +08:00
WDRemoteServerInfo info = m_pRemote->GetServerInfo();
if (info.ok)
{
updateStreamInfo(info);
emit serverInfoReceived(info);
}
const bool pullOk = startPullerWithRetry(m_streamUrl);
if (!pullOk)
2026-06-26 17:55:15 +08:00
{
emit statusMessage(QStringLiteral("服务端已开流,但本地 RTSP 多次拉流未启动: %1").arg(m_streamUrl));
return true;
2026-06-26 17:55:15 +08:00
}
emit statusMessage(QStringLiteral("Server 已开流RTSP 已出图: %1").arg(m_streamUrl));
2026-06-26 17:55:15 +08:00
}
return ok;
}
bool DroneScrewCtrlPresenter::StopLiveStream()
{
if (!m_pRemote) return false;
// 停止 RTSP 拉流
releasePuller();
bool ok = m_pRemote->StopWork(WDRemoteWorkMode::LiveStream) == 0;
if (ok) emit statusMessage(QStringLiteral("Server 已关流"));
return ok;
}
bool DroneScrewCtrlPresenter::StartDetection()
{
if (!m_pRemote) return false;
// 检测模式下停止 RTSP 拉流(改用 ZMQ 原始图像通道)
releasePuller();
clearCachedDisplayState();
2026-06-26 17:55:15 +08:00
m_pRemote->StopWork(WDRemoteWorkMode::LiveStream);
// 使用单目传输模式:仅接收左目图像,减少网络带宽消耗
m_pRemote->SetDetectMode("mono");
bool ok = m_pRemote->StartWork(WDRemoteWorkMode::Detection) == 0;
if (ok) emit statusMessage(QStringLiteral("Server 已开始检测(单目传输)"));
return ok;
}
bool DroneScrewCtrlPresenter::StopDetection()
{
if (!m_pRemote) return false;
bool ok = m_pRemote->StopWork(WDRemoteWorkMode::Detection) == 0;
if (ok) emit statusMessage(QStringLiteral("Server 已停止检测"));
return ok;
}
bool DroneScrewCtrlPresenter::StopAll()
{
if (!m_pRemote) return false;
// 停止 RTSP 拉流
releasePuller();
clearCachedDisplayState();
2026-06-26 17:55:15 +08:00
// 两种模式都停掉,保证服务端回到空闲(幂等)
m_pRemote->StopWork(WDRemoteWorkMode::Detection, 1000);
m_pRemote->StopWork(WDRemoteWorkMode::LiveStream, 1000);
emit statusMessage(QStringLiteral("Server 已停止(实时/检测)"));
return true;
}
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.modelPath = params.modelPath;
return m_pRemote->SetAlgoParams(p) == 0;
}
// =====================================================================
// WDRemoteReceiver 回调
// =====================================================================
void DroneScrewCtrlPresenter::onRemoteDetection(const WDRemoteDetectionFrame& f)
{
CtrlDetectionFrame frame = toCtrlFrame(f);
if (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)
return;
2026-06-26 17:55:15 +08:00
{
QMutexLocker lk(&m_lastResultMutex);
m_lastResult = frame;
m_bHasResult = true;
}
emit detectionResultReady(frame);
emit displayFrameReady(matchedFrame.image, matchedFrame.sourceSize, frame.frameId, false, frame, true);
2026-06-26 17:55:15 +08:00
}
void DroneScrewCtrlPresenter::onRemoteRawImage(const WDRemoteBinocularRawImage& img)
{
// 检测模式下板一通过 ZMQ 原始图通道发来左目图像(此时 RTSP 已关闭)。
// 转为 QImage走与 RTSP 相同的叠加+显示通路onOverlayFrameReady 会叠加检测框)。
if (!img.leftData || img.leftWidth <= 0 || img.leftHeight <= 0) return;
if (m_rtspReceiving.load()) return;
2026-06-26 17:55:15 +08:00
QImage q;
switch (img.leftPixelFormat)
{
case WDRemotePixelFormat::MONO8:
if (!hasReadableImageRows(img.leftData, img.leftWidth, img.leftHeight,
img.leftStride, img.leftDataSize, 1)) return;
2026-06-26 17:55:15 +08:00
q = QImage(reinterpret_cast<const uchar*>(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;
2026-06-26 17:55:15 +08:00
q = QImage(reinterpret_cast<const uchar*>(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;
2026-06-26 17:55:15 +08:00
q = QImage(reinterpret_cast<const uchar*>(img.leftData),
img.leftWidth, img.leftHeight, img.leftStride,
QImage::Format_RGB888).rgbSwapped();
break;
default:
return;
}
if (!q.isNull())
{
const qint64 frameId = static_cast<qint64>(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);
}
2026-06-26 17:55:15 +08:00
}
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<qint64>(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.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;
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;
2026-06-26 17:55:15 +08:00
QImage img;
if (frame.format == EFFPullPixelFormat::NV12)
{
if (!hasReadableRows(frame, 1)) return;
2026-06-26 17:55:15 +08:00
img = QImage(static_cast<const uchar*>(frame.data),
frame.width, frame.height, frame.stride,
QImage::Format_Grayscale8).copy();
}
else if (frame.format == EFFPullPixelFormat::RGBA32)
{
if (!hasReadableRows(frame, 4)) return;
2026-06-26 17:55:15 +08:00
img = QImage(static_cast<const uchar*>(frame.data),
frame.width, frame.height, frame.stride,
QImage::Format_RGBA8888).copy();
}
else if (frame.format == EFFPullPixelFormat::BGR24)
{
if (!hasReadableRows(frame, 3)) return;
2026-06-26 17:55:15 +08:00
img = QImage(static_cast<const uchar*>(frame.data),
frame.width, frame.height, frame.stride,
QImage::Format_BGR888).copy();
}
else
{
return;
}
if (!m_rtspFirstFrameReported.exchange(true))
emit statusMessage(QStringLiteral("RTSP 已出图"));
emit displayFrameReady(img, img.size(), -1, true, CtrlDetectionFrame{}, false);
2026-06-26 17:55:15 +08:00
}
void DroneScrewCtrlPresenter::onOverlayFrameReady(const QImage& img,
const QSize& sourceSize,
qint64 frameId,
bool rtspFrame,
const CtrlDetectionFrame& result,
bool hasResult)
2026-06-26 17:55:15 +08:00
{
QImage frame = img;
if (hasResult && !rtspFrame)
2026-06-26 17:55:15 +08:00
{
overlayDetectionBoxes(frame, result, sourceSize);
2026-06-26 17:55:15 +08:00
}
{
QMutexLocker lk(&m_lastImgMutex);
m_lastSourceImage = img;
m_lastSourceFrameId = frameId;
m_lastSourceIsRtsp = rtspFrame;
2026-06-26 17:55:15 +08:00
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)
2026-06-26 17:55:15 +08:00
{
if (!m_cfg.display.drawBoxes || frame.boxes.empty()) return;
QPainter p(&img);
p.setRenderHint(QPainter::Antialiasing, true);
const int thickness = m_cfg.display.boxThickness > 0 ? m_cfg.display.boxThickness : 1;
QPen pen(QColor(0, 0, 0), thickness);
2026-06-26 17:55:15 +08:00
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<double>(img.width()) / coordWidth
2026-06-26 17:55:15 +08:00
: 1.0;
const double scaleY = (coordHeight > 0)
? static_cast<double>(img.height()) / coordHeight
2026-06-26 17:55:15 +08:00
: 1.0;
const QRect imageRect(0, 0, img.width(), img.height());
2026-06-26 17:55:15 +08:00
for (size_t i = 0; i < frame.boxes.size(); ++i)
2026-06-26 17:55:15 +08:00
{
const auto& b = frame.boxes[i];
QRect r(qRound(static_cast<double>(b.x) * scaleX),
qRound(static_cast<double>(b.y) * scaleY),
qRound(static_cast<double>(b.width) * scaleX),
qRound(static_cast<double>(b.height) * scaleY));
r = r.normalized().intersected(imageRect);
if (r.isEmpty())
continue;
2026-06-26 17:55:15 +08:00
p.drawRect(r);
const QString label = QString::number(static_cast<int>(i + 1));
const QPoint labelPos(r.right() + 4, r.top() + fm.ascent());
p.drawText(labelPos, label);
2026-06-26 17:55:15 +08:00
}
}
QImage DroneScrewCtrlPresenter::GetLastDisplayImage() const
{
QMutexLocker lk(&m_lastImgMutex);
return m_lastDisplay;
}