#include "mainwindow.h" #include "ui_mainwindow.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace { QString matrixFirstRowText(const WDRemoteMatrix& matrix) { if (matrix.empty()) return QStringLiteral("N/A"); int count = matrix.cols; if (count > static_cast(matrix.data.size())) count = static_cast(matrix.data.size()); QStringList values; for (int i = 0; i < count; ++i) values << QString::number(matrix.data[static_cast(i)], 'f', 3); return values.join(QStringLiteral(", ")); } } MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); setWindowTitle(QStringLiteral("WDRemoteReceiver 测试(单目/双目图像)")); connect(ui->btnSearch, &QPushButton::clicked, this, &MainWindow::onSearchClicked); connect(ui->listDevices, &QListWidget::currentRowChanged, this, &MainWindow::onDeviceSelected); connect(ui->btnConnect, &QPushButton::clicked, this, &MainWindow::onConnectClicked); connect(ui->btnDisconnect, &QPushButton::clicked, this, &MainWindow::onDisconnectClicked); connect(ui->btnLiveStream, &QPushButton::clicked, this, &MainWindow::onLiveStreamClicked); connect(ui->btnStart, &QPushButton::clicked, this, &MainWindow::onStartWorkClicked); connect(ui->btnStop, &QPushButton::clicked, this, &MainWindow::onStopWorkClicked); connect(ui->btnSingle, &QPushButton::clicked, this, &MainWindow::onSingleClicked); connect(ui->btnSetExposure, &QPushButton::clicked, this, &MainWindow::onSetExposureClicked); connect(ui->btnSetGain, &QPushButton::clicked, this, &MainWindow::onSetGainClicked); connect(ui->btnSwapCameras, &QPushButton::clicked, this, &MainWindow::onSwapCamerasClicked); connect(ui->btnSetAlgo, &QPushButton::clicked, this, &MainWindow::onSetAlgoParamsClicked); connect(ui->btnGetInfo, &QPushButton::clicked, this, &MainWindow::onGetInfoClicked); connect(ui->btnCustom, &QPushButton::clicked, this, &MainWindow::onCustomCmdClicked); connect(ui->btnChooseSaveDir, &QPushButton::clicked, this, &MainWindow::onChooseSaveDirClicked); connect(ui->btnSaveImages, &QPushButton::clicked, this, &MainWindow::onSaveImagesClicked); QString baseSaveDir = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); if (baseSaveDir.isEmpty()) baseSaveDir = QDir::currentPath(); m_saveImageDir = QDir(baseSaveDir).filePath(QStringLiteral("WDRemoteReceiverImages")); ui->editSaveDir->setText(m_saveImageDir); // 图像传输模式选择(单目/双目) m_pDetectModeCombo = new QComboBox(this); m_pDetectModeCombo->addItem(QStringLiteral("双目图像 (binocular)"), QString("binocular")); m_pDetectModeCombo->addItem(QStringLiteral("单目图像 (mono)"), QString("mono")); m_pDetectModeCombo->setCurrentIndex(0); // 默认双目 m_pDetectModeCombo->setToolTip(QStringLiteral("选择检测模式下的图像传输方式\n" "mono: 仅发送左目图像\n" "binocular: 发送左右目两张图像")); // 把 combo 插入到 grpControl 布局中 QVBoxLayout* ctrlLayout = qobject_cast(ui->grpControl->layout()); if (ctrlLayout) { // 在第一个 spacer 之前插入 ctrlLayout->insertWidget(1, m_pDetectModeCombo); } connect(m_pDetectModeCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &MainWindow::onDetectModeChanged); // 跨线程:库回调(任意线程) → 主线程 UI connect(this, &MainWindow::uiAppendLog, this, &MainWindow::onAppendLog, Qt::QueuedConnection); connect(this, &MainWindow::uiShowDetection, this, &MainWindow::onShowDetection, Qt::QueuedConnection); connect(this, &MainWindow::uiShowLeftImage, this, &MainWindow::onShowLeftImage, Qt::QueuedConnection); connect(this, &MainWindow::uiShowRightImage, this, &MainWindow::onShowRightImage, Qt::QueuedConnection); connect(this, &MainWindow::uiUpdateConnectState, this, &MainWindow::onUpdateConnectState, Qt::QueuedConnection); setupReceiver(); ui->listDevices->addItem(QStringLiteral("未搜索到设备")); ui->btnConnect->setEnabled(false); QTimer::singleShot(100, this, &MainWindow::onSearchClicked); } MainWindow::~MainWindow() { releaseReceiver(); delete ui; } void MainWindow::setupReceiver() { IWDRemoteReceiver::CreateInstance(&m_pReceiver); if (!m_pReceiver) { QMessageBox::critical(this, "Error", "CreateInstance(IWDRemoteReceiver) failed"); return; } // 默认使用界面当前选择,避免初始化时把用户选择覆盖回双目。 const QString initialMode = m_pDetectModeCombo ? m_pDetectModeCombo->currentData().toString() : QStringLiteral("binocular"); m_monoImageMode.store(initialMode == "mono"); m_pReceiver->SetDetectMode(initialMode.toStdString()); if (initialMode == "mono") { QMutexLocker locker(&m_latestImageMutex); m_latestRightImage = QImage(); emit uiShowRightImage(QImage()); } m_pReceiver->SetEventCallback([this](WDRemoteEventType ev, const std::string& msg) { QString s; switch (ev) { case WDRemoteEventType::CONNECTED: s = "已连接"; emit uiUpdateConnectState(true); break; case WDRemoteEventType::DISCONNECTED: s = "已断开"; emit uiUpdateConnectState(false); break; case WDRemoteEventType::CONNECTION_ERROR: s = "连接错误"; emit uiUpdateConnectState(false); break; case WDRemoteEventType::REQUEST_TIMEOUT: s = "请求超时"; break; default: s = "未知事件"; break; } emit uiAppendLog(QString("[事件] %1 %2").arg(s, QString::fromStdString(msg))); }); m_pReceiver->SetDetectionCallback([this](const WDRemoteDetectionFrame& f) { QString s = QString("success=%1 code=%2 frame=%3 ts=%4 boxes=%5 wh=%6x%7 msg=%8") .arg(f.success ? 1 : 0) .arg(f.errorCode) .arg(f.frameId).arg(f.timestampUs) .arg(static_cast(f.boxes.size())) .arg(f.imageWidth).arg(f.imageHeight) .arg(QString::fromStdString(f.message)); emit uiShowDetection(s); }); // 原始图像回调:mono 模式下右目为空,只刷新左图并清空右图。 m_pReceiver->SetRawImageCallback([this](const WDRemoteBinocularRawImage& img) { // 左目 WDRemoteRawImage leftImg; leftImg.frameId = img.frameId; leftImg.timestampUs = img.timestampUs; leftImg.width = img.leftWidth; leftImg.height = img.leftHeight; leftImg.stride = img.leftStride; leftImg.pixelFormat = img.leftPixelFormat; leftImg.data = img.leftData; leftImg.dataSize = img.leftDataSize; QImage qLeft = rawImageToQImage(leftImg); if (!qLeft.isNull()) { { QMutexLocker locker(&m_latestImageMutex); m_latestLeftImage = qLeft; } emit uiShowLeftImage(qLeft); } // 右目(单目模式下右眼为空) if (!m_monoImageMode.load() && img.rightData && img.rightDataSize > 0) { WDRemoteRawImage rightImg; rightImg.frameId = img.frameId; rightImg.timestampUs = img.timestampUs; rightImg.width = img.rightWidth; rightImg.height = img.rightHeight; rightImg.stride = img.rightStride; rightImg.pixelFormat = img.rightPixelFormat; rightImg.data = img.rightData; rightImg.dataSize = img.rightDataSize; QImage qRight = rawImageToQImage(rightImg); if (!qRight.isNull()) { { QMutexLocker locker(&m_latestImageMutex); m_latestRightImage = qRight; } emit uiShowRightImage(qRight); } } else { { QMutexLocker locker(&m_latestImageMutex); m_latestRightImage = QImage(); } if (!m_monoImageMode.load()) emit uiShowRightImage(QImage()); } }); } void MainWindow::releaseReceiver() { if (m_pReceiver) { m_pReceiver->Disconnect(); delete m_pReceiver; m_pReceiver = nullptr; } } // ====================== UI 槽 ====================== void MainWindow::onSearchClicked() { if (!m_pReceiver) return; m_devices.clear(); ui->listDevices->clear(); ui->btnConnect->setEnabled(false); int r = m_pReceiver->SearchDevices(m_devices, 500); emit uiAppendLog(QString("SearchDevices ret=%1 count=%2") .arg(r) .arg(static_cast(m_devices.size()))); if (r != 0 || m_devices.empty()) { ui->listDevices->addItem(QStringLiteral("未搜索到设备")); return; } for (size_t i = 0; i < m_devices.size(); ++i) { const WDRemoteDeviceInfo& dev = m_devices[i]; const QString text = QString("%1 | %2 | %3 | ctrl:%4 result:%5 raw:%6 | det:%7fps stream:%8fps streamCam:%9 image:%10") .arg(QString::fromStdString(dev.deviceName)) .arg(QString::fromStdString(dev.machineCode)) .arg(QString::fromStdString(dev.serverIp)) .arg(dev.controlPort) .arg(dev.resultPort) .arg(dev.rawImagePort) .arg(dev.detectFps, 0, 'f', 1) .arg(dev.streamFps, 0, 'f', 1) .arg(QString::fromStdString(dev.streamCamera)) .arg(QString::fromStdString(dev.detectMode)); ui->listDevices->addItem(text); } ui->btnConnect->setEnabled(true); ui->listDevices->setCurrentRow(0); } void MainWindow::onDeviceSelected(int index) { if (index < 0 || index >= static_cast(m_devices.size())) { ui->btnConnect->setEnabled(false); return; } ui->btnConnect->setEnabled(true); const WDRemoteDeviceInfo& dev = m_devices[static_cast(index)]; emit uiAppendLog(QString("Selected device machine=%1 name=%2 ip=%3 ctrl=%4 result=%5 raw=%6") .arg(QString::fromStdString(dev.machineCode)) .arg(QString::fromStdString(dev.deviceName)) .arg(QString::fromStdString(dev.serverIp)) .arg(dev.controlPort) .arg(dev.resultPort) .arg(dev.rawImagePort)); } void MainWindow::onConnectClicked() { if (!m_pReceiver) return; if (m_devices.empty()) { emit uiAppendLog(QStringLiteral("Open skipped: search device first")); return; } int index = ui->listDevices->currentRow(); if (index < 0 || index >= static_cast(m_devices.size())) { emit uiAppendLog(QStringLiteral("Open skipped: select a device first")); return; } const WDRemoteDeviceInfo dev = m_devices[static_cast(index)]; int r = m_pReceiver->Open(dev.machineCode, 3000); emit uiAppendLog(QString("Open(machine=%1) ret=%2") .arg(QString::fromStdString(dev.machineCode)) .arg(r)); if (r == 0 && m_pDetectModeCombo) { const QString mode = m_pDetectModeCombo->currentData().toString(); m_monoImageMode.store(mode == "mono"); m_pReceiver->SetDetectMode(mode.toStdString()); if (mode == "mono") { QMutexLocker locker(&m_latestImageMutex); m_latestRightImage = QImage(); emit uiShowRightImage(QImage()); } emit uiAppendLog(QString("SetDetectMode after open=%1").arg(mode)); } } void MainWindow::onDisconnectClicked() { if (!m_pReceiver) return; int r = m_pReceiver->Disconnect(); emit uiAppendLog(QString("Disconnect ret=%1").arg(r)); } void MainWindow::onLiveStreamClicked() { if (!m_pReceiver) return; int r = m_pReceiver->StartWork(WDRemoteWorkMode::LiveStream); emit uiAppendLog(QString("StartWork(LiveStream) ret=%1").arg(r)); } void MainWindow::onStartWorkClicked() { if (!m_pReceiver) return; // 应用当前选择的检测模式 if (m_pDetectModeCombo) { QString mode = m_pDetectModeCombo->currentData().toString(); m_monoImageMode.store(mode == "mono"); m_pReceiver->SetDetectMode(mode.toStdString()); if (mode == "mono") { QMutexLocker locker(&m_latestImageMutex); m_latestRightImage = QImage(); emit uiShowRightImage(QImage()); } emit uiAppendLog(QString("SetDetectMode=%1").arg(mode)); } int r = m_pReceiver->StartWork(WDRemoteWorkMode::Detection); emit uiAppendLog(QString("StartWork(Detection) ret=%1").arg(r)); } void MainWindow::onStopWorkClicked() { if (!m_pReceiver) return; int r = m_pReceiver->StopWork(); emit uiAppendLog(QString("StopWork ret=%1 (已自动停止订阅)").arg(r)); } void MainWindow::onSingleClicked() { if (!m_pReceiver) return; if (m_singleInProgress) { emit uiAppendLog(QStringLiteral("Single ignored: request already running")); return; } m_singleInProgress = true; ui->btnSingle->setEnabled(false); int r = m_pReceiver->RequestSingleDetection(15000); emit uiAppendLog(QString("Single ret=%1").arg(r)); ui->btnSingle->setEnabled(true); m_singleInProgress = false; } void MainWindow::onSetExposureClicked() { if (!m_pReceiver) return; const WDRemoteServerInfo info = m_pReceiver->GetServerInfo(); const double current = info.ok ? (info.leftExposure > 0.0 ? info.leftExposure : (info.rightExposure > 0.0 ? info.rightExposure : 10000.0)) : 10000.0; const QString label = info.ok ? QStringLiteral("曝光时间(us),当前 L/R=%1/%2:") .arg(info.leftExposure, 0, 'f', 1) .arg(info.rightExposure, 0, 'f', 1) : QStringLiteral("曝光时间(us):"); if (!info.ok) emit uiAppendLog(QStringLiteral("GetServerInfo failed before SetExposure, use default value")); bool ok = false; double v = QInputDialog::getDouble(this, QStringLiteral("曝光时间(us)"), label, current, 0.0, 1e9, 3, &ok); if (!ok) return; int r = m_pReceiver->SetExposure(v); emit uiAppendLog(QString("SetExposure(%1) ret=%2").arg(v).arg(r)); } void MainWindow::onSetGainClicked() { if (!m_pReceiver) return; const WDRemoteServerInfo info = m_pReceiver->GetServerInfo(); const double current = info.ok ? (info.leftGain > 0.0 ? info.leftGain : (info.rightGain > 0.0 ? info.rightGain : 1.0)) : 1.0; const QString label = info.ok ? QStringLiteral("增益,当前 L/R=%1/%2:") .arg(info.leftGain, 0, 'f', 2) .arg(info.rightGain, 0, 'f', 2) : QStringLiteral("增益:"); if (!info.ok) emit uiAppendLog(QStringLiteral("GetServerInfo failed before SetGain, use default value")); bool ok = false; double v = QInputDialog::getDouble(this, QStringLiteral("增益"), label, current, 0.0, 1000.0, 3, &ok); if (!ok) return; int r = m_pReceiver->SetGain(v); emit uiAppendLog(QString("SetGain(%1) ret=%2").arg(v).arg(r)); } void MainWindow::onSetAlgoParamsClicked() { if (!m_pReceiver) return; WDRemoteAlgoParams p; p.scoreThreshold = 0.5f; p.nmsThreshold = 0.45f; p.inputWidth = 640; p.inputHeight = 640; p.modelType = 0; int r = m_pReceiver->SetAlgoParams(p); emit uiAppendLog(QString("SetAlgoParams ret=%1").arg(r)); } void MainWindow::onGetInfoClicked() { if (!m_pReceiver) return; WDRemoteServerInfo info = m_pReceiver->GetServerInfo(); QString s = QString("GetInfo ok=%1 mode=%2 imageMode=%3 streamCam=%4 rtsp=%5 raw=%6 result=%7 ctrl=%8 detFps=%9 streamFps=%10 stream=%11x%12 exp(L/R)=%13/%14 gain(L/R)=%15/%16") .arg(info.ok) .arg(QString::fromStdString(info.mode)) .arg(QString::fromStdString(info.detectMode)) .arg(QString::fromStdString(info.streamCamera)) .arg(QString::fromStdString(info.rtspUrl)) .arg(info.rawImagePort) .arg(info.resultPort) .arg(info.controlPort) .arg(info.detectFps, 0, 'f', 1) .arg(info.streamFps, 0, 'f', 1) .arg(info.streamWidth) .arg(info.streamHeight) .arg(info.leftExposure, 0, 'f', 1) .arg(info.rightExposure, 0, 'f', 1) .arg(info.leftGain, 0, 'f', 2) .arg(info.rightGain, 0, 'f', 2); emit uiAppendLog(s); emit uiAppendLog(QString("Camera L index=%1 serial=%2 | R index=%3 serial=%4") .arg(info.leftCameraIndex) .arg(QString::fromStdString(info.leftCameraSerial)) .arg(info.rightCameraIndex) .arg(QString::fromStdString(info.rightCameraSerial))); const WDRemoteCalibrationInfo& calib = info.calibration; emit uiAppendLog(QString("Calibration valid=%1 source=%2 msg=%3") .arg(calib.valid ? 1 : 0) .arg(QString::fromStdString(calib.sourcePath)) .arg(QString::fromStdString(calib.message))); emit uiAppendLog(QString("Calibration L valid=%1 image=%2x%3 rms=%4 K[0]=[%5]") .arg(calib.left.valid ? 1 : 0) .arg(calib.left.imageWidth) .arg(calib.left.imageHeight) .arg(calib.left.rms, 0, 'f', 6) .arg(matrixFirstRowText(calib.left.cameraMatrix))); emit uiAppendLog(QString("Calibration R valid=%1 image=%2x%3 rms=%4 K[0]=[%5]") .arg(calib.right.valid ? 1 : 0) .arg(calib.right.imageWidth) .arg(calib.right.imageHeight) .arg(calib.right.rms, 0, 'f', 6) .arg(matrixFirstRowText(calib.right.cameraMatrix))); emit uiAppendLog(QString("Calibration stereo valid=%1 baselineMm=%2 R=%3x%4 Q=%5x%6") .arg(calib.stereo.valid ? 1 : 0) .arg(calib.stereo.baselineMm, 0, 'f', 3) .arg(calib.stereo.R.rows) .arg(calib.stereo.R.cols) .arg(calib.stereo.Q.rows) .arg(calib.stereo.Q.cols)); } void MainWindow::onCustomCmdClicked() { if (!m_pReceiver) return; bool ok = false; QString req = QInputDialog::getText(this, QStringLiteral("自定义 JSON 命令"), QStringLiteral("JSON:"), QLineEdit::Normal, R"({"cmd":"get_info"})", &ok); if (!ok || req.isEmpty()) return; std::string resp; int r = m_pReceiver->SendCustomCommand(req.toStdString(), resp); emit uiAppendLog(QString("Custom ret=%1 resp=%2").arg(r).arg(QString::fromStdString(resp))); } void MainWindow::onSwapCamerasClicked() { if (!m_pReceiver) return; const auto ret = QMessageBox::question( this, QStringLiteral("交换左右目"), QStringLiteral("请先停止检测和实时传图。\n确认交换服务端逻辑 left/right 相机配置并重连相机?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (ret != QMessageBox::Yes) return; std::string resp; const int r = m_pReceiver->SendCustomCommand(R"({"cmd":"swap_cameras"})", resp, 10000); emit uiAppendLog(QString("SwapCameras ret=%1 resp=%2") .arg(r) .arg(QString::fromStdString(resp))); if (r == 0) onGetInfoClicked(); } void MainWindow::onChooseSaveDirClicked() { QString current = ui->editSaveDir->text().trimmed(); if (current.isEmpty()) current = m_saveImageDir.isEmpty() ? QDir::currentPath() : m_saveImageDir; const QString dir = QFileDialog::getExistingDirectory( this, QStringLiteral("选择图像保存路径"), current); if (dir.isEmpty()) return; m_saveImageDir = dir; ui->editSaveDir->setText(dir); emit uiAppendLog(QString("Save image dir=%1").arg(dir)); } void MainWindow::onSaveImagesClicked() { QString dirPath = ui->editSaveDir->text().trimmed(); if (dirPath.isEmpty()) dirPath = m_saveImageDir.isEmpty() ? QDir::currentPath() : m_saveImageDir; QDir dir(dirPath); if (!dir.exists() && !QDir().mkpath(dirPath)) { emit uiAppendLog(QString("Save images failed: create dir %1").arg(dirPath)); return; } m_saveImageDir = dirPath; QImage left; QImage right; { QMutexLocker locker(&m_latestImageMutex); left = m_latestLeftImage; right = m_latestRightImage; } if (left.isNull() && right.isNull()) { emit uiAppendLog(QStringLiteral("Save images skipped: no image received")); return; } int seq = m_saveImageSeq; QString seqText; do { seqText = QString::number(seq); ++seq; } while (dir.exists(QString("LeftImage_%1.png").arg(seqText)) || dir.exists(QString("RightImage_%1.png").arg(seqText))); m_saveImageSeq = seq; bool ok = true; QStringList saved; if (!left.isNull()) { const QString path = dir.filePath(QString("LeftImage_%1.png").arg(seqText)); ok = left.save(path, "PNG") && ok; saved << path; } if (!right.isNull()) { const QString path = dir.filePath(QString("RightImage_%1.png").arg(seqText)); ok = right.save(path, "PNG") && ok; saved << path; } emit uiAppendLog(QString("Save images %1: %2") .arg(ok ? QStringLiteral("ok") : QStringLiteral("failed")) .arg(saved.join(QStringLiteral(", ")))); } void MainWindow::onDetectModeChanged(int index) { if (!m_pReceiver || !m_pDetectModeCombo) return; QString mode = m_pDetectModeCombo->itemData(index).toString(); m_monoImageMode.store(mode == "mono"); m_pReceiver->SetDetectMode(mode.toStdString()); if (mode == "mono") { QMutexLocker locker(&m_latestImageMutex); m_latestRightImage = QImage(); emit uiShowRightImage(QImage()); } emit uiAppendLog(QString("DetectMode changed to: %1 (image=%2)") .arg(mode, mode == "mono" ? QStringLiteral("仅左目") : QStringLiteral("左右目"))); } // ====================== UI 显示 ====================== void MainWindow::onAppendLog(const QString& line) { if (ui->textLog) { ui->textLog->append(line); } } void MainWindow::onShowDetection(const QString& summary) { emit uiAppendLog(QString("[结果] %1").arg(summary)); } void MainWindow::onShowLeftImage(const QImage& img) { if (img.isNull()) return; QPixmap pm = QPixmap::fromImage(img); if (pm.size() != ui->labelLeftImage->size()) pm = pm.scaled(ui->labelLeftImage->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); ui->labelLeftImage->setPixmap(pm); } void MainWindow::onShowRightImage(const QImage& img) { if (m_monoImageMode.load() || img.isNull()) { ui->labelRightImage->clear(); ui->labelRightImage->setText(QStringLiteral("单目图像模式:未传输右图像")); return; } QPixmap pm = QPixmap::fromImage(img); if (pm.size() != ui->labelRightImage->size()) pm = pm.scaled(ui->labelRightImage->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); ui->labelRightImage->setPixmap(pm); } void MainWindow::onUpdateConnectState(bool connected) { ui->lblConnState->setText(connected ? QStringLiteral("已连接") : QStringLiteral("已断开")); ui->lblConnState->setStyleSheet(connected ? "color: green;" : "color: red;"); } // ====================== 工具 ====================== QImage MainWindow::rawImageToQImage(const WDRemoteRawImage& img) { if (!img.data || img.width <= 0 || img.height <= 0) return QImage(); // 这里仅处理常见三种格式;BayerRG8 / NV12 留给上层后续按需扩展 switch (img.pixelFormat) { case WDRemotePixelFormat::MONO8: { QImage q(img.data, img.width, img.height, img.stride, QImage::Format_Grayscale8); return q.copy(); } case WDRemotePixelFormat::RGB8: { QImage q(img.data, img.width, img.height, img.stride, QImage::Format_RGB888); return q.copy(); } case WDRemotePixelFormat::BGR8: { QImage q(img.data, img.width, img.height, img.stride, QImage::Format_RGB888); return q.rgbSwapped(); } default: return QImage(); } }