螺栓视频流和检测,接下来调优存储

This commit is contained in:
杰仔 2026-06-30 11:12:25 +08:00
parent 92ef1af588
commit 818aa764fb
15 changed files with 406 additions and 166 deletions

View File

@ -45,6 +45,10 @@ isEmpty(TARGET_APP) {
SUBDIRS += BinocularMark/BinocularMark.pro
} else:equals(TARGET_APP, "WorkpieceProject") {
SUBDIRS += WorkpieceProject/WorkpieceProject.pro
} else:equals(TARGET_APP, "WorkpiecePosition") {
SUBDIRS += WorkpieceProject/WorkpieceProject.pro
} else:equals(TARGET_APP, "WorkpieceSplice") {
SUBDIRS += WorkpieceProject/WorkpieceProject.pro
} else:equals(TARGET_APP, "TunnelChannel") {
SUBDIRS += TunnelChannel/TunnelChannel.pro
} else:equals(TARGET_APP, "WheelMeasure") {

View File

@ -1,6 +1,17 @@
TEMPLATE = subdirs
# 撕裂项目
SUBDIRS += BeltTearingConfig/BeltTearingConfig.pro
SUBDIRS += BeltTearingApp/BeltTearingApp.pro
SUBDIRS += BeltTearingServer/BeltTearingServer.pro
BeltTearingConfig.file = BeltTearingConfig/BeltTearingConfig.pro
BeltTearingApp.file = BeltTearingApp/BeltTearingApp.pro
BeltTearingServer.file = BeltTearingServer/BeltTearingServer.pro
SUBDIRS += BeltTearingConfig
equals(TARGET_APP, "BeltTearing") {
SUBDIRS += BeltTearingServer
BeltTearingServer.depends = BeltTearingConfig
} else {
SUBDIRS += BeltTearingApp
SUBDIRS += BeltTearingServer
BeltTearingApp.depends = BeltTearingConfig
BeltTearingServer.depends = BeltTearingConfig
}

View File

@ -80,8 +80,8 @@ private slots:
private:
// RTSP 拉流回调
void onRtspFrame(const VrFFPulledFrame& frame);
bool initPuller(const QString& streamUrl);
bool startPullerOnce(const QString& streamUrl);
bool initPuller(const QString& streamUrl, bool quiet = false);
bool startPullerOnce(const QString& streamUrl, bool quiet = false);
bool waitForRtspFirstFrame(int timeoutMs);
bool startPullerWithRetry(const QString& streamUrl);
void releasePuller();
@ -128,6 +128,7 @@ private:
std::atomic<bool> m_bConnected{false};
std::atomic<bool> m_bRunning{false};
std::atomic<bool> m_liveStreamMode{false};
std::atomic<bool> m_rtspReceiving{false};
std::atomic<bool> m_rtspFirstFrameReported{false};
QTimer* m_pReconnectTimer{nullptr};

View File

@ -11,15 +11,18 @@
#include <QThread>
#include <QUrl>
#include <QtGlobal>
#include <algorithm>
namespace
{
constexpr int kDefaultRtspPullPort = 8554;
constexpr int kLegacyRtmpPublishPort = 1935;
constexpr unsigned int kRtspOpenTimeoutMs = 5000;
constexpr int kRtspPullMaxAttempts = 5;
constexpr unsigned int kRtspOpenTimeoutMs = 10000;
constexpr int kRtspPullMaxAttempts = 20;
constexpr int kRtspPullRetryDelayMs = 500;
constexpr int kRtspFirstFrameWaitMs = 5000;
constexpr int kRtspFirstFrameWaitMs = 10000;
constexpr int kControlStartTimeoutMs = 30000;
constexpr int kControlStopTimeoutMs = 30000;
constexpr size_t kMaxPendingMatchFrames = 30;
constexpr qint64 kMaxPendingMatchAgeMs = 15000;
@ -124,8 +127,9 @@ QSize overlayCoordinateSize(const CtrlDetectionFrame& frame, const QImage& img,
{
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());
if (frame.imageWidth > 0 && frame.imageHeight > 0)
return QSize(frame.imageWidth, frame.imageHeight);
return img.size();
}
qint64 nowMs()
@ -261,6 +265,7 @@ void DroneScrewCtrlPresenter::DeinitApp()
{
if (m_pReconnectTimer && m_pReconnectTimer->isActive()) m_pReconnectTimer->stop();
m_liveStreamMode = false;
releasePuller();
if (m_pRemote)
@ -307,20 +312,22 @@ QString DroneScrewCtrlPresenter::resolveStreamUrl(const QString& streamUrl) cons
return normalizeStreamUrl(streamUrl, m_cfg);
}
bool DroneScrewCtrlPresenter::initPuller(const QString& streamUrl)
bool DroneScrewCtrlPresenter::initPuller(const QString& streamUrl, bool quiet)
{
releasePuller();
const QString url = resolveStreamUrl(streamUrl);
if (url.isEmpty())
{
emit statusMessage(QStringLiteral("Server 未返回拉流地址"));
if (!quiet)
emit statusMessage(QStringLiteral("Server 未返回拉流地址"));
return false;
}
if (!IVrFFMediaPuller::CreateObject(&m_pPuller) || !m_pPuller)
{
emit statusMessage(QStringLiteral("创建拉流器失败"));
if (!quiet)
emit statusMessage(QStringLiteral("创建拉流器失败"));
return false;
}
@ -339,7 +346,8 @@ bool DroneScrewCtrlPresenter::initPuller(const QString& streamUrl)
const int ret = m_pPuller->Init(pc);
if (ret != 0)
{
emit statusMessage(QStringLiteral("打开拉流地址失败 ret=%1: %2").arg(ret).arg(url));
if (!quiet)
emit statusMessage(QStringLiteral("打开拉流地址失败 ret=%1: %2").arg(ret).arg(url));
releasePuller();
return false;
}
@ -347,19 +355,23 @@ bool DroneScrewCtrlPresenter::initPuller(const QString& streamUrl)
return true;
}
bool DroneScrewCtrlPresenter::startPullerOnce(const QString& streamUrl)
bool DroneScrewCtrlPresenter::startPullerOnce(const QString& streamUrl, bool quiet)
{
const QString url = resolveStreamUrl(streamUrl);
if (url.isEmpty())
{
emit statusMessage(QStringLiteral("Server 未返回拉流地址,配置也无法生成地址"));
if (!quiet)
emit statusMessage(QStringLiteral("Server 未返回拉流地址,配置也无法生成地址"));
return false;
}
m_streamUrl = url;
emit statusMessage(QStringLiteral("正在打开拉流: %1").arg(url));
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 20);
if (initPuller(url))
if (!quiet)
{
emit statusMessage(QStringLiteral("正在打开拉流: %1").arg(url));
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 20);
}
if (initPuller(url, quiet))
{
m_rtspFirstFrameReported = false;
m_rtspReceiving = true;
@ -367,15 +379,18 @@ bool DroneScrewCtrlPresenter::startPullerOnce(const QString& streamUrl)
if (startRet == 0)
{
m_streamUrl = url;
emit statusMessage(QStringLiteral("拉流器已启动: %1").arg(url));
if (!quiet)
emit statusMessage(QStringLiteral("拉流器已启动: %1").arg(url));
return true;
}
m_rtspReceiving = false;
emit statusMessage(QStringLiteral("启动拉流器失败 ret=%1: %2").arg(startRet).arg(url));
if (!quiet)
emit statusMessage(QStringLiteral("启动拉流器失败 ret=%1: %2").arg(startRet).arg(url));
releasePuller();
}
emit statusMessage(QStringLiteral("启动拉流失败: %1").arg(url));
if (!quiet)
emit statusMessage(QStringLiteral("启动拉流失败: %1").arg(url));
return false;
}
@ -405,33 +420,24 @@ bool DroneScrewCtrlPresenter::startPullerWithRetry(const QString& streamUrl)
}
m_streamUrl = url;
emit statusMessage(QStringLiteral("正在拉流: %1").arg(url));
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 20);
for (int attempt = 1; attempt <= kRtspPullMaxAttempts; ++attempt)
{
emit statusMessage(QStringLiteral("正在拉流(%1/%2): %3")
.arg(attempt)
.arg(kRtspPullMaxAttempts)
.arg(url));
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 20);
const bool started = startPullerOnce(url, true);
if (startPullerOnce(url) && waitForRtspFirstFrame(kRtspFirstFrameWaitMs))
if (started && 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);
}
}
emit statusMessage(QStringLiteral("本地 RTSP 拉流未收到首帧Server 推流保持运行: %1").arg(url));
return false;
}
@ -445,12 +451,15 @@ bool DroneScrewCtrlPresenter::StartLiveStream()
if (!m_pRemote) return false;
releasePuller();
clearCachedDisplayState();
m_pRemote->StopWork(WDRemoteWorkMode::Detection);
m_liveStreamMode = false;
emit statusMessage(QStringLiteral("正在请求服务端启动距离检测..."));
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 20);
bool ok = m_pRemote->StartWork(WDRemoteWorkMode::LiveStream) == 0;
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();
@ -463,60 +472,85 @@ bool DroneScrewCtrlPresenter::StartLiveStream()
const bool pullOk = startPullerWithRetry(m_streamUrl);
if (!pullOk)
{
emit statusMessage(QStringLiteral("服务端已开流,但本地 RTSP 多次拉流未启动: %1").arg(m_streamUrl));
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();
bool ok = m_pRemote->StopWork(WDRemoteWorkMode::LiveStream) == 0;
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->StopWork(WDRemoteWorkMode::LiveStream);
// 使用单目传输模式:仅接收左目图像,减少网络带宽消耗
m_pRemote->SetDetectMode("mono");
bool ok = m_pRemote->StartWork(WDRemoteWorkMode::Detection) == 0;
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;
bool ok = m_pRemote->StopWork(WDRemoteWorkMode::Detection) == 0;
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();
// 两种模式都停掉,保证服务端回到空闲(幂等)
m_pRemote->StopWork(WDRemoteWorkMode::Detection, 1000);
m_pRemote->StopWork(WDRemoteWorkMode::LiveStream, 1000);
emit statusMessage(QStringLiteral("Server 已停止(实时/检测)"));
return true;
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)
@ -548,7 +582,7 @@ bool DroneScrewCtrlPresenter::PushAlgoParams(const DroneScrewAlgoUiParams& param
void DroneScrewCtrlPresenter::onRemoteDetection(const WDRemoteDetectionFrame& f)
{
CtrlDetectionFrame frame = toCtrlFrame(f);
if (m_rtspReceiving.load())
if (m_liveStreamMode.load() || m_rtspReceiving.load())
{
{
QMutexLocker lk(&m_lastResultMutex);
@ -579,7 +613,15 @@ void DroneScrewCtrlPresenter::onRemoteDetection(const WDRemoteDetectionFrame& f)
}
if (!hasMatchedImage)
{
{
QMutexLocker lk(&m_lastResultMutex);
m_lastResult = frame;
m_bHasResult = true;
}
emit detectionResultReady(frame);
return;
}
{
QMutexLocker lk(&m_lastResultMutex);
@ -595,7 +637,7 @@ void DroneScrewCtrlPresenter::onRemoteRawImage(const WDRemoteBinocularRawImage&
// 检测模式下板一通过 ZMQ 原始图通道发来左目图像(此时 RTSP 已关闭)。
// 转为 QImage走与 RTSP 相同的叠加+显示通路onOverlayFrameReady 会叠加检测框)。
if (!img.leftData || img.leftWidth <= 0 || img.leftHeight <= 0) return;
if (m_rtspReceiving.load()) return;
if (m_liveStreamMode.load() || m_rtspReceiving.load()) return;
QImage q;
switch (img.leftPixelFormat)
@ -649,7 +691,9 @@ void DroneScrewCtrlPresenter::onRemoteRawImage(const WDRemoteBinocularRawImage&
}
if (!hasMatchedResult)
{
return;
}
{
QMutexLocker lk(&m_lastResultMutex);
@ -757,9 +801,21 @@ void DroneScrewCtrlPresenter::onRtspFrame(const VrFFPulledFrame& frame)
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, CtrlDetectionFrame{}, false);
emit displayFrameReady(img, img.size(), -1, true, latestResult, hasLatestResult);
}
void DroneScrewCtrlPresenter::onOverlayFrameReady(const QImage& img,
@ -770,9 +826,16 @@ void DroneScrewCtrlPresenter::onOverlayFrameReady(const QImage& img,
bool hasResult)
{
QImage frame = img;
if (hasResult && !rtspFrame)
if (hasResult && !m_liveStreamMode.load())
{
overlayDetectionBoxes(frame, result, sourceSize);
QSize overlaySourceSize = sourceSize;
if (rtspFrame)
{
overlaySourceSize = (result.imageWidth > 0 && result.imageHeight > 0)
? QSize(result.imageWidth, result.imageHeight)
: img.size();
}
overlayDetectionBoxes(frame, result, overlaySourceSize);
}
{
@ -839,9 +902,10 @@ void DroneScrewCtrlPresenter::overlayDetectionBoxes(QImage& img,
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;
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);
@ -864,10 +928,15 @@ void DroneScrewCtrlPresenter::overlayDetectionBoxes(QImage& img,
for (size_t i = 0; i < frame.boxes.size(); ++i)
{
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));
if (b.width <= 0 || b.height <= 0)
continue;
const int x1 = qRound(static_cast<double>(b.x) * scaleX);
const int y1 = qRound(static_cast<double>(b.y) * scaleY);
int x2 = qRound(static_cast<double>(b.x + b.width) * scaleX) - 1;
int y2 = qRound(static_cast<double>(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;

View File

@ -25,7 +25,7 @@
<Display drawBoxes="true"
drawScores="true"
drawClassId="true"
boxThickness="1"
boxThickness="2"
maxResultListItems="200" />
</DroneScrewCtrlConfig>

View File

@ -556,9 +556,16 @@ void MainWindow::on_btn_start_clicked()
ui->btn_start->setEnabled(false);
ui->btn_detect_start->setEnabled(false);
QApplication::processEvents();
if (m_pPresenter) m_pPresenter->StopAll();
appendLog(QStringLiteral("已请求 Server 停止"));
applyUiState(UiState::Idle);
const bool ok = m_pPresenter && m_pPresenter->StopAll();
if (ok)
{
appendLog(QStringLiteral("已请求 Server 停止"));
applyUiState(UiState::Idle);
}
else
{
appendLog(QStringLiteral("Server 停止失败,状态未切换"));
}
ui->btn_start->setEnabled(true);
ui->btn_detect_start->setEnabled(true);
}

View File

@ -51,6 +51,17 @@ QString distanceText(const CtrlDetectionFrame& frame, int targetIndex)
? QStringLiteral("未测到距离")
: distanceParts.join(" ");
}
QString distanceIndexText(const CtrlDetectionFrame& frame, int targetIndex)
{
const int total = static_cast<int>(frame.distances.size());
if (targetIndex >= 0 && targetIndex < total)
{
const CtrlDetectionDistance& d = frame.distances[static_cast<size_t>(targetIndex)];
return QString::number(d.toId);
}
return QStringLiteral("-");
}
}
ResultItem::ResultItem(QWidget* parent)
@ -112,11 +123,7 @@ void ResultItem::setResultData(int targetIndex,
.arg(static_cast<int>(frame.boxes.size())));
else if (mode == DisplayMode::Distance)
{
const int total = static_cast<int>(frame.distances.size());
if (targetIndex >= 0 && targetIndex < total)
ui->label_id->setText(QString("%1/%2").arg(targetIndex + 1).arg(total));
else
ui->label_id->setText(QString("%1/%1").arg(total));
ui->label_id->setText(distanceIndexText(frame, targetIndex));
}
else
ui->label_id->setText(QStringLiteral("0/0"));

View File

@ -1323,6 +1323,7 @@ int DroneScrewServerPresenter::initRtspPusher(unsigned int width, unsigned int h
void DroneScrewServerPresenter::releaseRtspPusher()
{
std::lock_guard<std::mutex> pusherLock(m_rtspPusherMutex);
m_bRtspStarted = false;
if (m_pPusher)
{
@ -1343,6 +1344,67 @@ void DroneScrewServerPresenter::releaseRtspPusher()
m_rtspUvInitialized = false;
m_rtspRgaScaleDisabled = false;
m_rtspRgaScaleLogged = false;
resetRtspPushState();
}
int DroneScrewServerPresenter::startRtspPusher()
{
std::lock_guard<std::mutex> pusherLock(m_rtspPusherMutex);
if (!m_pPusher)
return ERR_CODE(DRONESCREW_ERR_RTSP_INIT);
return m_pPusher->Start();
}
void DroneScrewServerPresenter::resetRtspPushState()
{
{
std::lock_guard<std::mutex> lk(m_rtspPushMutex);
m_rtspPushedFrameCounter = 0;
m_rtspLastPushRet = 0;
m_rtspLastPushFrameId = 0;
}
}
void DroneScrewServerPresenter::recordRtspPushResult(int ret, unsigned long long frameId)
{
bool logFirstSuccess = false;
bool logFirstFailure = false;
int64_t pushedCount = 0;
const bool pendingEncoderOutput = (ret == ERR_CODE(DEV_RESULT_EMPTY));
{
std::lock_guard<std::mutex> lk(m_rtspPushMutex);
pushedCount = m_rtspPushedFrameCounter.load();
const int previousRet = m_rtspLastPushRet.load();
m_rtspLastPushFrameId = frameId;
if (ret == 0)
{
m_rtspLastPushRet = 0;
++m_rtspPushedFrameCounter;
logFirstSuccess = (pushedCount == 0);
}
else if (!pendingEncoderOutput)
{
m_rtspLastPushRet = ret;
logFirstFailure = (!pendingEncoderOutput &&
pushedCount == 0 &&
previousRet != ret);
}
else if (previousRet == 0)
{
m_rtspLastPushRet = ret;
}
}
if (logFirstSuccess)
{
LOG_INFO("[RTMP] first frame pushed frame=%llu pull=%s\n",
frameId, m_rtspAdvertiseUrl.toStdString().c_str());
}
else if (logFirstFailure)
{
LOG_WARN("[RTMP] push failed before first frame ret=%d frame=%llu\n",
ret, frameId);
}
}
void DroneScrewServerPresenter::leftCameraCallback(const MvsImageData& img)
@ -1406,12 +1468,12 @@ void DroneScrewServerPresenter::leftCameraCallback(const MvsImageData& img)
// 直播相机配置为左目时,收到左目立即推流(不等右目)。
if (isLiveStreamCameraRole("left") &&
m_pPusher && m_bRtspStarted.load() && img.pData)
m_bRtspStarted.load() && img.pData)
{
const int rtspRet = pushRtspFrame(img);
if (rtspRet == 0)
++rtspPushCnt;
else
else if (rtspRet != ERR_CODE(DEV_RESULT_EMPTY))
++rtspFailCnt;
}
@ -1443,47 +1505,49 @@ void DroneScrewServerPresenter::rightCameraCallback(const MvsImageData& img)
{
try
{
QMutexLocker lk(&m_frameMutex);
// 检查是否需要重新分配内存
if (m_rightImageData.dataSize != img.dataSize)
{
if (m_rightImageData.pData != nullptr)
{
delete[] m_rightImageData.pData;
m_rightImageData.pData = nullptr;
LOG_DEBUG("[CAM] right realloc: old=%zu new=%zu frame=%llu\n",
m_rightImageData.dataSize, img.dataSize, img.frameID);
}
QMutexLocker lk(&m_frameMutex);
if (img.dataSize > 0)
// 检查是否需要重新分配内存
if (m_rightImageData.dataSize != img.dataSize)
{
m_rightImageData.pData = new (std::nothrow) unsigned char[img.dataSize];
if (!m_rightImageData.pData)
if (m_rightImageData.pData != nullptr)
{
m_rightImageData.dataSize = 0;
m_bRightImageReady = false;
LOG_ERROR("[CAM] right alloc failed size=%zu frame=%llu\n",
img.dataSize, img.frameID);
return;
delete[] m_rightImageData.pData;
m_rightImageData.pData = nullptr;
LOG_DEBUG("[CAM] right realloc: old=%zu new=%zu frame=%llu\n",
m_rightImageData.dataSize, img.dataSize, img.frameID);
}
if (img.dataSize > 0)
{
m_rightImageData.pData = new (std::nothrow) unsigned char[img.dataSize];
if (!m_rightImageData.pData)
{
m_rightImageData.dataSize = 0;
m_bRightImageReady = false;
LOG_ERROR("[CAM] right alloc failed size=%zu frame=%llu\n",
img.dataSize, img.frameID);
return;
}
}
}
// 复制图像数据
m_rightImageData.width = img.width;
m_rightImageData.height = img.height;
m_rightImageData.dataSize = img.dataSize;
m_rightImageData.pixelFormat = img.pixelFormat;
m_rightImageData.frameID = img.frameID;
m_rightImageData.timestamp = img.timestamp;
if (m_rightImageData.pData != nullptr && img.pData != nullptr)
{
memcpy(m_rightImageData.pData, img.pData, img.dataSize);
}
m_bRightImageReady = true;
}
// 复制图像数据
m_rightImageData.width = img.width;
m_rightImageData.height = img.height;
m_rightImageData.dataSize = img.dataSize;
m_rightImageData.pixelFormat = img.pixelFormat;
m_rightImageData.frameID = img.frameID;
m_rightImageData.timestamp = img.timestamp;
if (m_rightImageData.pData != nullptr && img.pData != nullptr)
{
memcpy(m_rightImageData.pData, img.pData, img.dataSize);
}
m_bRightImageReady = true;
static bool rightFirst = true;
if (rightFirst) {
std::ostringstream oss;
@ -1496,12 +1560,12 @@ void DroneScrewServerPresenter::rightCameraCallback(const MvsImageData& img)
static int rtspFailCnt = 0;
if (isLiveStreamCameraRole("right") &&
m_pPusher && m_bRtspStarted.load() && img.pData)
m_bRtspStarted.load() && img.pData)
{
const int rtspRet = pushRtspFrame(img);
if (rtspRet == 0)
++rtspPushCnt;
else
else if (rtspRet != ERR_CODE(DEV_RESULT_EMPTY))
++rtspFailCnt;
}
@ -1711,10 +1775,10 @@ int DroneScrewServerPresenter::startDetectionWork()
}
// 停止 RTSP 推流(检测和推流互斥)
if (m_pPusher && m_bRtspStarted.load())
if (m_pPusher)
{
LOG_DEBUG("[DETECT] stopping RTSP pusher (detection mode)\n");
m_pPusher->Stop();
releaseRtspPusher();
}
m_bRtspStarted = false;
resetFrameReadyFlags();
@ -1746,12 +1810,12 @@ int DroneScrewServerPresenter::stopDetectionWork()
return 0;
}
m_bIsDetecting = false;
stopGpioTriggerLoop();
m_bThreadExit = true;
if (m_detectThread.joinable())
m_detectThread.join();
stopImageSaveThread();
m_bIsDetecting = false;
m_detectPipelineMode = kDetectPipelinePrecision;
m_activeTriggerFps = static_cast<int>(kPrecisionFrameRate);
@ -1768,6 +1832,8 @@ int DroneScrewServerPresenter::startLiveStream()
return 0;
}
releaseRtspPusher();
if (m_bIsDetecting.load())
{
LOG_WARN("[LIVE] start rejected: detection is running\n");
@ -1802,6 +1868,9 @@ int DroneScrewServerPresenter::startLiveStream()
if (acqRet != 0)
{
emit statusChanged(QStringLiteral("实时传图相机采集启动失败"));
releaseRtspPusher();
m_detectPipelineMode = kDetectPipelinePrecision;
m_activeTriggerFps = static_cast<int>(kPrecisionFrameRate);
return acqRet;
}
@ -1815,20 +1884,25 @@ int DroneScrewServerPresenter::startLiveStream()
LOG_ERROR("[LIVE] RTSP pusher init failed ret=%d\n", ret);
stopLiveCameras();
releaseRtspPusher();
m_detectPipelineMode = kDetectPipelinePrecision;
m_activeTriggerFps = static_cast<int>(kPrecisionFrameRate);
return ERR_CODE(DRONESCREW_ERR_RTSP_INIT);
}
ret = m_pPusher->Start();
ret = startRtspPusher();
if (ret != 0)
{
LOG_ERROR("[LIVE] RTSP pusher start failed ret=%d\n", ret);
m_bRtspStarted = false;
stopLiveCameras();
releaseRtspPusher();
m_detectPipelineMode = kDetectPipelinePrecision;
m_activeTriggerFps = static_cast<int>(kPrecisionFrameRate);
return ERR_CODE(DRONESCREW_ERR_RTSP_START);
}
m_rtspFrameCounter = 0; // 重置帧计数器
resetRtspPushState();
m_bRtspStarted = true;
m_bLiveStreaming = true;
m_bThreadExit = false;
@ -1851,7 +1925,6 @@ int DroneScrewServerPresenter::stopLiveStream()
releaseRtspPusher();
else
m_bRtspStarted = false;
stopImageSaveThread();
m_rtspFrameCounter = 0; // 重置计数器,防止下次开流时 DTS 残留
return 0;
}
@ -2255,11 +2328,6 @@ QString DroneScrewServerPresenter::imageSaveModeName() const
if (m_detectPipelineMode.load() == kDetectPipelineDistance)
return QStringLiteral("distance");
const std::string mode = detectMode();
if (mode == "mono")
return QStringLiteral("precision_mono");
if (mode == "binocular")
return QStringLiteral("precision_binocular");
return QStringLiteral("precision");
}
@ -2267,9 +2335,13 @@ void DroneScrewServerPresenter::startImageSaveThread(const QString& modeName)
{
stopImageSaveThread();
const QString dateDir = QDateTime::currentDateTime().toString(QStringLiteral("yyyyMMdd"));
const QDateTime now = QDateTime::currentDateTime();
const QString dateDir = now.toString(QStringLiteral("yyyyMMdd"));
const QString sessionName = QStringLiteral("%1_%2")
.arg(modeName,
now.toString(QStringLiteral("HHmmss_zzz")));
const QString sessionDir =
QDir(QDir(QString::fromLatin1(kImageSaveRootDir)).filePath(dateDir)).filePath(modeName);
QDir(QDir(QString::fromLatin1(kImageSaveRootDir)).filePath(dateDir)).filePath(sessionName);
if (!QDir().mkpath(sessionDir))
{
LOG_WARN("[SAVE] create image save dir failed: %s\n",
@ -2294,9 +2366,9 @@ void DroneScrewServerPresenter::stopImageSaveThread()
const bool shouldJoin = m_imageSaveThread.joinable();
{
std::lock_guard<std::mutex> lk(m_imageSaveMutex);
m_imageSaveQueue.clear();
if (!shouldJoin)
{
m_imageSaveQueue.clear();
m_imageSaveSessionDir.clear();
m_imageSaveThreadExit = false;
return;
@ -2397,12 +2469,11 @@ void DroneScrewServerPresenter::imageSaveThreadFunc()
return m_imageSaveThreadExit.load() || !m_imageSaveQueue.empty();
});
if (m_imageSaveThreadExit.load())
break;
if (m_imageSaveQueue.empty())
{
if (m_imageSaveThreadExit.load())
break;
continue;
}
job = std::move(m_imageSaveQueue.front());
m_imageSaveQueue.pop_front();
@ -2422,7 +2493,7 @@ void DroneScrewServerPresenter::imageSaveThreadFunc()
static_cast<int>(img.width),
QImage::Format_Grayscale8);
const QString filePath = QDir(job.dirPath).filePath(
QStringLiteral("%1Image_%2.png").arg(prefix).arg(job.index));
QStringLiteral("%1_%2Image.png").arg(job.index).arg(prefix));
if (!q.save(filePath, "PNG"))
{
LOG_WARN("[SAVE] image save failed: %s\n",
@ -3142,20 +3213,34 @@ int DroneScrewServerPresenter::pushRtspFrame(const MvsImageData& img)
{
try
{
std::lock_guard<std::mutex> pusherLock(m_rtspPusherMutex);
if (!m_pPusher) return ERR_CODE(DRONESCREW_ERR_RTSP_INIT);
if (!m_bLiveStreaming.load() || !m_bRtspStarted.load()) return 0;
if (!img.pData || img.dataSize == 0 || img.width == 0 || img.height == 0)
return ERR_CODE(DATA_ERR_INVALID);
{
const int ret = ERR_CODE(DATA_ERR_INVALID);
recordRtspPushResult(ret, img.frameID);
return ret;
}
const unsigned int outWidth = m_rtspWidth > 0 ? m_rtspWidth : img.width;
const unsigned int outHeight = m_rtspHeight > 0 ? m_rtspHeight : img.height;
if (outWidth == 0 || outHeight == 0)
return ERR_CODE(DATA_ERR_INVALID);
{
const int ret = ERR_CODE(DATA_ERR_INVALID);
recordRtspPushResult(ret, img.frameID);
return ret;
}
if (img.pixelFormat == kMvsPixelTypeMono8)
{
const size_t monoSize = static_cast<size_t>(img.width) * static_cast<size_t>(img.height);
if (img.dataSize < monoSize) return ERR_CODE(DATA_ERR_LEN);
if (img.dataSize < monoSize)
{
const int ret = ERR_CODE(DATA_ERR_LEN);
recordRtspPushResult(ret, img.frameID);
return ret;
}
const size_t ySize = static_cast<size_t>(outWidth) *
static_cast<size_t>(outHeight);
@ -3236,27 +3321,39 @@ int DroneScrewServerPresenter::pushRtspFrame(const MvsImageData& img)
const int64_t ptsUs = static_cast<int64_t>(
(static_cast<double>(frameIdx) / streamFps) * 1000000.0
);
return m_pPusher->PushFrame(m_rtspFrameBuffer.data(), m_rtspFrameBuffer.size(), ptsUs);
const int ret = m_pPusher->PushFrame(m_rtspFrameBuffer.data(),
m_rtspFrameBuffer.size(),
ptsUs);
recordRtspPushResult(ret, img.frameID);
return ret;
}
return ERR_CODE(DATA_ERR_INVALID);
const int ret = ERR_CODE(DATA_ERR_INVALID);
recordRtspPushResult(ret, img.frameID);
return ret;
}
catch (const std::bad_alloc& e)
{
LOG_ERROR("[RTSP] push memory allocation failed frame=%llu src=%ux%u: %s\n",
img.frameID, img.width, img.height, e.what());
return ERR_CODE(DATA_ERR_MEM);
const int ret = ERR_CODE(DATA_ERR_MEM);
recordRtspPushResult(ret, img.frameID);
return ret;
}
catch (const std::exception& e)
{
LOG_ERROR("[RTSP] push exception frame=%llu src=%ux%u: %s\n",
img.frameID, img.width, img.height, e.what());
return ERR_CODE(DATA_ERR_INVALID);
const int ret = ERR_CODE(DATA_ERR_INVALID);
recordRtspPushResult(ret, img.frameID);
return ret;
}
catch (...)
{
LOG_ERROR("[RTSP] push unknown exception frame=%llu src=%ux%u\n",
img.frameID, img.width, img.height);
return ERR_CODE(DATA_ERR_INVALID);
const int ret = ERR_CODE(DATA_ERR_INVALID);
recordRtspPushResult(ret, img.frameID);
return ret;
}
}

View File

@ -210,8 +210,11 @@ private:
int prepareLiveStreamAcquisition();
void resetFrameReadyFlags();
int initRtspPusher(unsigned int width, unsigned int height, unsigned int fps);
int startRtspPusher();
void releaseRtspPusher();
int pushRtspFrame(const MvsImageData& img);
void resetRtspPushState();
void recordRtspPushResult(int ret, unsigned long long frameId);
bool tryRgaScaleMonoToY(const MvsImageData& img,
unsigned char* dstY,
unsigned int outWidth,
@ -308,6 +311,11 @@ private:
std::atomic<int> m_detectPipelineMode{0}; // 0=precision, 1=distance
std::atomic<int> m_activeTriggerFps{2};
std::atomic<int64_t> m_rtspFrameCounter{0}; // 相对帧计数(推流启动时重置)
std::mutex m_rtspPusherMutex;
std::mutex m_rtspPushMutex;
std::atomic<int64_t> m_rtspPushedFrameCounter{0};
std::atomic<int> m_rtspLastPushRet{0};
std::atomic<unsigned long long> m_rtspLastPushFrameId{0};
std::vector<unsigned char> m_rtspFrameBuffer;
std::vector<unsigned int> m_rtspScaleX;
std::vector<unsigned int> m_rtspScaleY;

View File

@ -25,7 +25,7 @@
namespace
{
constexpr int kRawImageZlibLevel = 9;
constexpr bool kUseFixedPrecisionMonoRawMjpeg = false;
constexpr bool kUseFixedPrecisionMonoRawMjpeg = true;
constexpr int kFixedPrecisionMonoRawMjpegQuality = 25;
constexpr int kFixedPrecisionMonoRawScaleDiv = 4;

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@ -1,9 +1,21 @@
# WorkpieceProject.pro
# 工件项目总工程文件
TEMPLATE = subdirs
SUBDIRS += \
WorkpieceProjectConfig/WorkpieceProjectConfig.pro \
WorkpiecePositionApp/WorkpiecePositionApp.pro \
WorkpieceSpliceApp/WorkpieceSpliceApp.pro
WorkpieceProjectConfig.file = WorkpieceProjectConfig/WorkpieceProjectConfig.pro
WorkpiecePositionApp.file = WorkpiecePositionApp/WorkpiecePositionApp.pro
WorkpieceSpliceApp.file = WorkpieceSpliceApp/WorkpieceSpliceApp.pro
SUBDIRS += WorkpieceProjectConfig
equals(TARGET_APP, "WorkpiecePosition") {
SUBDIRS += WorkpiecePositionApp
WorkpiecePositionApp.depends = WorkpieceProjectConfig
} else:equals(TARGET_APP, "WorkpieceSplice") {
SUBDIRS += WorkpieceSpliceApp
WorkpieceSpliceApp.depends = WorkpieceProjectConfig
} else {
SUBDIRS += WorkpiecePositionApp
SUBDIRS += WorkpieceSpliceApp
WorkpiecePositionApp.depends = WorkpieceProjectConfig
WorkpieceSpliceApp.depends = WorkpieceProjectConfig
}

View File

@ -585,7 +585,7 @@ int CVrFFMediaPusher::PushFrame(const void* data, size_t size, int64_t ptsUs)
mpp_packet_deinit(&packet);
mpp_buffer_put(pktBuf);
LOG_DEBUG("FFMediaPusher: encode produced no packet (warmup)\n");
return SUCCESS; // 跳过,不算失败
return ERR_CODE(DEV_RESULT_EMPTY);
}
MppBuffer encodedBuf = mpp_packet_get_buffer(encodedPacket);
@ -618,7 +618,7 @@ int CVrFFMediaPusher::PushFrame(const void* data, size_t size, int64_t ptsUs)
{
releaseEncodedPacket();
LOG_DEBUG("FFMediaPusher: encode produced empty packet data\n");
return SUCCESS; // 跳过,不算失败
return ERR_CODE(DEV_RESULT_EMPTY);
}
// 4) 写入 RTSP独立 AVPacketpts rescale 到 muxer 时基)
@ -678,8 +678,7 @@ int CVrFFMediaPusher::PushFrame(const void* data, size_t size, int64_t ptsUs)
// 微秒 → muxer 实际时基
av_packet_rescale_ts(avpkt, AVRational{1, 1000000},
m_fmtCtx->streams[0]->time_base);
// Single video stream: write directly to avoid interleave buffering latency.
aret = av_write_frame(m_fmtCtx, avpkt);
aret = av_interleaved_write_frame(m_fmtCtx, avpkt);
// RTMP 推流需要强制刷新缓冲区,否则数据会积压不发送
if (aret >= 0 && m_fmtCtx->pb)
@ -689,7 +688,7 @@ int CVrFFMediaPusher::PushFrame(const void* data, size_t size, int64_t ptsUs)
}
else
{
aret = 0; // 已停止,丢弃本帧
aret = AVERROR_EOF; // 已停止,丢弃本帧
}
}
@ -697,7 +696,7 @@ int CVrFFMediaPusher::PushFrame(const void* data, size_t size, int64_t ptsUs)
if (aret < 0)
{
LOG_DEBUG("FFMediaPusher: av_write_frame fail %d\n", aret);
LOG_DEBUG("FFMediaPusher: av_interleaved_write_frame fail %d\n", aret);
return ERR_CODE(DEV_CTRL_ERR);
}

View File

@ -3,27 +3,59 @@
TEMPLATE = subdirs
SUBDIRS += \
ShareMem \
BinocularMarkReceiver \
ModbusTCPServer \
ModbusTCPClient \
AuthModule \
HandEyeCalib \
ChessboardDetector \
FFMediaStream \
ZeroMQClient \
ZeroMQServer \
ZeroMQPubSub \
WDRemoteReceiver
isEmpty(TARGET_APP) {
SUBDIRS += \
ShareMem \
BinocularMarkReceiver \
ModbusTCPServer \
ModbusTCPClient \
AuthModule \
HandEyeCalib \
ChessboardDetector \
FFMediaStream \
ZeroMQClient \
ZeroMQServer \
ZeroMQPubSub \
WDRemoteReceiver
} else {
SUBDIRS += \
ShareMem \
BinocularMarkReceiver \
ModbusTCPServer \
AuthModule
contains(TARGET_APP, "^(BagThreadPosition|HoleDetection|StatorPosition|WorkpieceHole)$") {
SUBDIRS += ModbusTCPClient
}
contains(TARGET_APP, "^(ScrewPosition|WorkpieceHole|DiscHolePose|TireHolePose|RodAndBarPosition|RodWeldSeam)$") {
SUBDIRS += HandEyeCalib
}
contains(TARGET_APP, "^(DroneScrewServer|DroneScrewCtrlApp|DroneScrewbolt)$") {
SUBDIRS += FFMediaStream ZeroMQPubSub
}
contains(TARGET_APP, "^(DroneScrewServer|DroneScrewbolt)$") {
SUBDIRS += ZeroMQServer
}
contains(TARGET_APP, "^(DroneScrewCtrlApp|DroneScrewbolt)$") {
SUBDIRS += ZeroMQClient WDRemoteReceiver
}
}
# 设置依赖关系
# 注意SUBDIRS 必须用短名目录名注册qmake 会自动查找 子目录/同名.pro
# 这样 .depends 才能生效若写成 "WDRemoteReceiver/WDRemoteReceiver.pro" 路径形式,
# 该依赖不会被 qmake 关联make -j 并行编译时会出现 "cannot find -lZeroMQClient"
# WDRemoteReceiver 是动态库,链接 ZeroMQClient / ZeroMQPubSub 静态库,必须先构建它们
WDRemoteReceiver.depends = ZeroMQClient ZeroMQPubSub
contains(SUBDIRS, WDRemoteReceiver) {
WDRemoteReceiver.depends = ZeroMQClient ZeroMQPubSub AuthModule
}
win32-msvc {
SUBDIRS += CloudShow
}
SUBDIRS = $$unique(SUBDIRS)

View File

@ -686,13 +686,9 @@ int WDRemoteReceiver::StartWork(int timeoutMs)
int WDRemoteReceiver::StartWork(WDRemoteWorkMode mode, int timeoutMs)
{
std::lock_guard<std::mutex> stateLock(m_mtxWorkState);
const WorkState oldState = m_workState;
if (mode == WDRemoteWorkMode::LiveStream)
{
if (oldState == WorkState::LiveStream)
return 0;
Json::Value stopReq;
stopReq["cmd"] = "stop";
Json::FastWriter w;
@ -734,9 +730,6 @@ int WDRemoteReceiver::StartWork(WDRemoteWorkMode mode, int timeoutMs)
return 0;
}
if (oldState == WorkState::Detection)
return 0;
Json::Value stopStreamReq;
stopStreamReq["cmd"] = "stop_stream";
Json::FastWriter w;