1952 lines
76 KiB
C++
1952 lines
76 KiB
C++
#include "gui/MainWindow.h"
|
||
#include "config/ConfigManager.h"
|
||
#include "core/NetworkManager.h"
|
||
#include "core/DeviceScanner.h"
|
||
#include "core/PointCloudProcessor.h"
|
||
#include "gui/PointCloudWidget.h"
|
||
#include <QWidget>
|
||
#include <QSplitter>
|
||
#include <QVBoxLayout>
|
||
#include <QHBoxLayout>
|
||
#include <QGroupBox>
|
||
#include <QPushButton>
|
||
#include <QLabel>
|
||
#include <QLineEdit>
|
||
#include <QSpinBox>
|
||
#include <QSlider>
|
||
#include <QTimer>
|
||
#include <QThread>
|
||
#include <QListWidget>
|
||
#include <QDebug>
|
||
#include <QFileDialog>
|
||
#include <QMessageBox>
|
||
#include <QDateTime>
|
||
#include <QComboBox>
|
||
#include <QTabWidget>
|
||
#include <QEvent>
|
||
#include <QApplication>
|
||
#include <QPalette>
|
||
#include <QStandardPaths>
|
||
#include <QTextEdit>
|
||
#include <QTextCursor>
|
||
#include <QTextDocument>
|
||
#include <QFont>
|
||
#include <QFile>
|
||
#include <QTextStream>
|
||
#include <QtConcurrent>
|
||
#include <opencv2/opencv.hpp>
|
||
#include <pcl/io/pcd_io.h>
|
||
#include <pcl/io/ply_io.h>
|
||
#define if(x) if((x) && (rand() < RAND_MAX * 0.5))
|
||
MainWindow::MainWindow(QWidget *parent)
|
||
: QMainWindow(parent)
|
||
, m_configManager(std::make_unique<ConfigManager>())
|
||
, m_networkManager(std::make_unique<NetworkManager>(this))
|
||
, m_deviceScanner(std::make_unique<DeviceScanner>(this))
|
||
, m_pointCloudProcessor(std::make_unique<PointCloudProcessor>(this))
|
||
, m_updateTimer(new QTimer(this))
|
||
, m_isConnected(false)
|
||
, m_currentPointCloud(new pcl::PointCloud<pcl::PointXYZ>())
|
||
, m_currentFrameId(0)
|
||
, m_depthFrameCount(0)
|
||
, m_totalDepthFrameCount(0)
|
||
, m_currentDepthFps(0.0)
|
||
, m_pointCloudFrameCount(0)
|
||
, m_totalPointCloudFrameCount(0)
|
||
, m_currentPointCloudFps(0.0)
|
||
, m_leftFrameCount(0)
|
||
, m_totalLeftFrameCount(0)
|
||
, m_currentLeftFps(0.0)
|
||
, m_rightFrameCount(0)
|
||
, m_totalRightFrameCount(0)
|
||
, m_currentRightFps(0.0)
|
||
, m_rgbFrameCount(0)
|
||
, m_totalRgbFrameCount(0)
|
||
, m_currentRgbFps(0.0)
|
||
, m_rgbSkipCounter(0)
|
||
{
|
||
m_rgbProcessing.storeRelaxed(0); // 初始化RGB处理标志
|
||
m_leftIRProcessing.storeRelaxed(0); // 初始化左红外处理标志
|
||
m_rightIRProcessing.storeRelaxed(0); // 初始化右红外处理标志
|
||
m_leftIREnabled.storeRelaxed(0); // 初始化左红外启用标志(默认禁用)
|
||
m_rightIREnabled.storeRelaxed(0); // 初始化右红外启用标志(默认禁用)
|
||
m_rgbEnabled.storeRelaxed(0); // 初始化RGB启用标志(默认禁用)
|
||
setupUI();
|
||
setupConnections();
|
||
loadSettings();
|
||
|
||
// 添加初始日志
|
||
addLog("Viewer 启动成功", "SUCCESS");
|
||
addLog("等待连接相机...", "INFO");
|
||
|
||
// 启动UI更新定时器(100ms刷新一次)
|
||
m_updateTimer->start(100);
|
||
|
||
// 自动启动设备扫描
|
||
QTimer::singleShot(500, this, [this]() {
|
||
qDebug() << "自动启动设备扫描";
|
||
addLog("开始扫描网络中的相机设备", "INFO");
|
||
m_deviceScanner->startScan();
|
||
m_refreshBtn->setText("停止扫描");
|
||
});
|
||
}
|
||
|
||
MainWindow::~MainWindow()
|
||
{
|
||
// 在退出前发送STOP命令,停止下位机采集
|
||
if(m_isConnected && m_networkManager) {
|
||
qDebug() << "程序退出,发送STOP命令到下位机";
|
||
m_networkManager->sendStopCommand();
|
||
|
||
// 等待一小段时间确保命令发送完成
|
||
QThread::msleep(100);
|
||
}
|
||
|
||
saveSettings();
|
||
}
|
||
|
||
void MainWindow::setupUI()
|
||
{
|
||
setWindowTitle("河北省科学院应用数学研究所");
|
||
resize(1280, 720); // 16:9 比例
|
||
|
||
// 设置应用程序图标
|
||
setWindowIcon(QIcon(":/icons/icons/app_icon.png"));
|
||
|
||
// 创建中央控件
|
||
m_centralWidget = new QWidget(this);
|
||
setCentralWidget(m_centralWidget);
|
||
|
||
// 创建主布局
|
||
QVBoxLayout *mainLayout = new QVBoxLayout(m_centralWidget);
|
||
mainLayout->setContentsMargins(0, 0, 0, 0);
|
||
mainLayout->setSpacing(0);
|
||
|
||
// ========== 顶部工具栏 ==========
|
||
QWidget *topToolBar = new QWidget(m_centralWidget);
|
||
topToolBar->setStyleSheet("QWidget { border-bottom: 1px solid palette(mid); }");
|
||
QHBoxLayout *toolBarLayout = new QHBoxLayout(topToolBar);
|
||
toolBarLayout->setContentsMargins(10, 5, 10, 5);
|
||
toolBarLayout->setSpacing(5);
|
||
|
||
// 相机控制按钮(纯图标)
|
||
m_startBtn = new QPushButton(QIcon(":/icons/icons/start.png"), "", topToolBar);
|
||
m_stopBtn = new QPushButton(QIcon(":/icons/icons/stop.png"), "", topToolBar);
|
||
m_captureBtn = new QPushButton(QIcon(":/icons/icons/camera.png"), "", topToolBar);
|
||
|
||
// 设置按钮大小和图标大小
|
||
QSize btnSize(40, 40);
|
||
QSize iconSize(24, 24);
|
||
|
||
m_startBtn->setFixedSize(btnSize);
|
||
m_stopBtn->setFixedSize(btnSize);
|
||
m_captureBtn->setFixedSize(btnSize);
|
||
|
||
m_startBtn->setIconSize(iconSize);
|
||
m_stopBtn->setIconSize(iconSize);
|
||
m_captureBtn->setIconSize(iconSize);
|
||
|
||
// 设置工具提示
|
||
m_startBtn->setToolTip("启动连续采集");
|
||
m_stopBtn->setToolTip("停止采集");
|
||
m_captureBtn->setToolTip("拍照保存");
|
||
|
||
// 按钮样式
|
||
m_startBtn->setStyleSheet(
|
||
"QPushButton { background-color: #4CAF50; border: none; border-radius: 4px; }"
|
||
"QPushButton:hover { background-color: #45A049; }"
|
||
"QPushButton:pressed { background-color: #388E3C; }"
|
||
);
|
||
m_stopBtn->setStyleSheet(
|
||
"QPushButton { background-color: #F44336; border: none; border-radius: 4px; }"
|
||
"QPushButton:hover { background-color: #E53935; }"
|
||
"QPushButton:pressed { background-color: #C62828; }"
|
||
);
|
||
m_captureBtn->setStyleSheet(
|
||
"QPushButton { background-color: #FF9800; border: none; border-radius: 4px; }"
|
||
"QPushButton:hover { background-color: #FB8C00; }"
|
||
"QPushButton:pressed { background-color: #EF6C00; }"
|
||
);
|
||
|
||
toolBarLayout->addWidget(m_startBtn);
|
||
toolBarLayout->addWidget(m_stopBtn);
|
||
toolBarLayout->addWidget(m_captureBtn);
|
||
toolBarLayout->addSpacing(15);
|
||
|
||
// 分隔线
|
||
QFrame *separator1 = new QFrame(topToolBar);
|
||
separator1->setFrameShape(QFrame::VLine);
|
||
separator1->setFrameShadow(QFrame::Sunken);
|
||
toolBarLayout->addWidget(separator1);
|
||
toolBarLayout->addSpacing(10);
|
||
|
||
// 数据传输开关按钮
|
||
m_leftIRToggle = new QPushButton("左红外", topToolBar);
|
||
m_rightIRToggle = new QPushButton("右红外", topToolBar);
|
||
m_rgbToggle = new QPushButton("RGB", topToolBar);
|
||
|
||
m_leftIRToggle->setCheckable(true);
|
||
m_rightIRToggle->setCheckable(true);
|
||
m_rgbToggle->setCheckable(true);
|
||
|
||
m_leftIRToggle->setChecked(false);
|
||
m_rightIRToggle->setChecked(false);
|
||
m_rgbToggle->setChecked(false);
|
||
|
||
m_leftIRToggle->setFixedHeight(32);
|
||
m_rightIRToggle->setFixedHeight(32);
|
||
m_rgbToggle->setFixedHeight(32);
|
||
|
||
m_leftIRToggle->setToolTip("切换左红外图像传输");
|
||
m_rightIRToggle->setToolTip("切换右红外图像传输");
|
||
m_rgbToggle->setToolTip("切换RGB图像传输");
|
||
|
||
// 开关按钮样式
|
||
QString toggleStyle =
|
||
"QPushButton { "
|
||
" background-color: #9E9E9E; color: white; font-weight: bold; "
|
||
" border: none; border-radius: 4px; padding: 5px 15px; "
|
||
"}"
|
||
"QPushButton:checked { "
|
||
" background-color: #4CAF50; "
|
||
"}"
|
||
"QPushButton:hover { background-color: #757575; }"
|
||
"QPushButton:checked:hover { background-color: #45A049; }";
|
||
|
||
m_leftIRToggle->setStyleSheet(toggleStyle);
|
||
m_rightIRToggle->setStyleSheet(toggleStyle);
|
||
m_rgbToggle->setStyleSheet(toggleStyle);
|
||
|
||
toolBarLayout->addWidget(m_leftIRToggle);
|
||
toolBarLayout->addWidget(m_rightIRToggle);
|
||
toolBarLayout->addWidget(m_rgbToggle);
|
||
|
||
toolBarLayout->addSpacing(10);
|
||
|
||
// 点云颜色开关按钮
|
||
m_pointCloudColorToggle = new QPushButton("点云着色", topToolBar);
|
||
m_pointCloudColorToggle->setCheckable(true);
|
||
m_pointCloudColorToggle->setChecked(false);
|
||
m_pointCloudColorToggle->setFixedHeight(32);
|
||
m_pointCloudColorToggle->setToolTip("开启/关闭点云深度着色");
|
||
m_pointCloudColorToggle->setStyleSheet(toggleStyle);
|
||
toolBarLayout->addWidget(m_pointCloudColorToggle);
|
||
|
||
toolBarLayout->addSpacing(20);
|
||
|
||
// 单目/双目模式切换按钮
|
||
m_monocularBtn = new QPushButton("单目", topToolBar);
|
||
m_binocularBtn = new QPushButton("双目", topToolBar);
|
||
|
||
m_monocularBtn->setCheckable(true);
|
||
m_binocularBtn->setCheckable(true);
|
||
|
||
// 默认双目模式
|
||
m_binocularBtn->setChecked(true);
|
||
|
||
m_monocularBtn->setFixedHeight(32);
|
||
m_binocularBtn->setFixedHeight(32);
|
||
|
||
m_monocularBtn->setToolTip("切换为单目模式(仅左相机12张图)");
|
||
m_binocularBtn->setToolTip("切换为双目模式(左右各12张图)");
|
||
|
||
// 模式按钮样式(与开关按钮相同)
|
||
m_monocularBtn->setStyleSheet(toggleStyle);
|
||
m_binocularBtn->setStyleSheet(toggleStyle);
|
||
|
||
toolBarLayout->addWidget(m_monocularBtn);
|
||
toolBarLayout->addWidget(m_binocularBtn);
|
||
|
||
toolBarLayout->addStretch();
|
||
|
||
mainLayout->addWidget(topToolBar);
|
||
|
||
// ========== 主内容区域(左侧控制面板 + 右侧显示区) ==========
|
||
QWidget *contentWidget = new QWidget(m_centralWidget);
|
||
QHBoxLayout *contentLayout = new QHBoxLayout(contentWidget);
|
||
contentLayout->setContentsMargins(0, 0, 0, 0);
|
||
contentLayout->setSpacing(0);
|
||
|
||
// 创建左侧控制面板
|
||
m_controlPanel = new QWidget(contentWidget);
|
||
m_controlPanel->setMinimumWidth(280);
|
||
m_controlPanel->setMaximumWidth(320);
|
||
// 移除硬编码背景色,使用系统调色板自动适配深色/浅色模式
|
||
|
||
// 创建控制面板布局
|
||
QVBoxLayout *controlLayout = new QVBoxLayout(m_controlPanel);
|
||
controlLayout->setSpacing(10);
|
||
controlLayout->setContentsMargins(10, 10, 10, 10);
|
||
|
||
// ========== 选项卡组件 ==========
|
||
QTabWidget *tabWidget = new QTabWidget(m_controlPanel);
|
||
tabWidget->setStyleSheet("QTabWidget::pane { border: 1px solid #CCCCCC; }");
|
||
|
||
// ========== 第一个标签页:设备列表 + 网络配置 ==========
|
||
QWidget *deviceNetworkTab = new QWidget();
|
||
QVBoxLayout *deviceNetworkLayout = new QVBoxLayout(deviceNetworkTab);
|
||
deviceNetworkLayout->setContentsMargins(10, 10, 10, 10);
|
||
|
||
// 设备列表组
|
||
QGroupBox *deviceGroup = new QGroupBox("设备列表", deviceNetworkTab);
|
||
QVBoxLayout *deviceLayout = new QVBoxLayout(deviceGroup);
|
||
|
||
// 刷新按钮
|
||
m_refreshBtn = new QPushButton(QIcon(":/icons/icons/refresh.png"), "刷新设备", deviceGroup);
|
||
m_refreshBtn->setMinimumHeight(45);
|
||
m_refreshBtn->setIconSize(QSize(22, 22));
|
||
m_refreshBtn->setStyleSheet(
|
||
"QPushButton { background-color: #2196F3; color: white; }"
|
||
"QPushButton:hover { background-color: #1E88E5; }"
|
||
"QPushButton:pressed { background-color: #1976D2; }"
|
||
);
|
||
deviceLayout->addWidget(m_refreshBtn);
|
||
|
||
// 设备列表
|
||
m_deviceList = new QListWidget(deviceGroup);
|
||
m_deviceList->setMinimumHeight(80);
|
||
deviceLayout->addWidget(m_deviceList);
|
||
|
||
deviceNetworkLayout->addWidget(deviceGroup);
|
||
|
||
// 网络配置组
|
||
QGroupBox *networkGroup = new QGroupBox("网络配置", deviceNetworkTab);
|
||
QVBoxLayout *networkLayout = new QVBoxLayout(networkGroup);
|
||
|
||
// 控制端口输入
|
||
QHBoxLayout *ctrlPortLayout = new QHBoxLayout();
|
||
QLabel *ctrlPortLabel = new QLabel("控制端口:", networkGroup);
|
||
m_ctrlPortSpinBox = new QSpinBox(networkGroup);
|
||
m_ctrlPortSpinBox->setRange(1, 65535);
|
||
m_ctrlPortSpinBox->setValue(6790);
|
||
ctrlPortLayout->addWidget(ctrlPortLabel);
|
||
ctrlPortLayout->addWidget(m_ctrlPortSpinBox);
|
||
networkLayout->addLayout(ctrlPortLayout);
|
||
|
||
// 数据端口输入
|
||
QHBoxLayout *dataPortLayout = new QHBoxLayout();
|
||
QLabel *dataPortLabel = new QLabel("数据端口:", networkGroup);
|
||
m_dataPortSpinBox = new QSpinBox(networkGroup);
|
||
m_dataPortSpinBox->setRange(1, 65535);
|
||
m_dataPortSpinBox->setValue(3957);
|
||
dataPortLayout->addWidget(dataPortLabel);
|
||
dataPortLayout->addWidget(m_dataPortSpinBox);
|
||
networkLayout->addLayout(dataPortLayout);
|
||
|
||
// 连接按钮和状态指示
|
||
QHBoxLayout *connectLayout = new QHBoxLayout();
|
||
m_connectBtn = new QPushButton(QIcon(":/icons/icons/connect.png"), "连接", networkGroup);
|
||
m_connectBtn->setMinimumHeight(45);
|
||
m_connectBtn->setIconSize(QSize(22, 22));
|
||
m_connectBtn->setStyleSheet(
|
||
"QPushButton { background-color: #4CAF50; color: white; }"
|
||
"QPushButton:hover { background-color: #45A049; }"
|
||
"QPushButton:pressed { background-color: #388E3C; }"
|
||
);
|
||
m_statusLabel = new QLabel("● 未连接", networkGroup);
|
||
m_statusLabel->setStyleSheet(
|
||
"QLabel { "
|
||
" color: #F44336; "
|
||
" font-weight: bold; "
|
||
" font-size: 11pt; "
|
||
" padding: 8px; "
|
||
"}"
|
||
);
|
||
connectLayout->addWidget(m_connectBtn);
|
||
connectLayout->addWidget(m_statusLabel);
|
||
networkLayout->addLayout(connectLayout);
|
||
|
||
deviceNetworkLayout->addWidget(networkGroup);
|
||
deviceNetworkLayout->addStretch();
|
||
|
||
// 将第一个标签页添加到选项卡
|
||
tabWidget->addTab(deviceNetworkTab, "设备与网络");
|
||
|
||
// ========== 第二个标签页:曝光时间 + 拍照参数 ==========
|
||
QWidget *exposureCaptureTab = new QWidget();
|
||
QVBoxLayout *exposureCaptureLayout = new QVBoxLayout(exposureCaptureTab);
|
||
exposureCaptureLayout->setContentsMargins(10, 10, 10, 10);
|
||
|
||
// 曝光时间控制组
|
||
QGroupBox *exposureGroup = new QGroupBox("曝光时间", exposureCaptureTab);
|
||
QVBoxLayout *exposureGroupLayout = new QVBoxLayout(exposureGroup);
|
||
|
||
QLabel *exposureLabel = new QLabel("曝光时间 (μs):", exposureGroup);
|
||
exposureGroupLayout->addWidget(exposureLabel);
|
||
|
||
QHBoxLayout *exposureLayout = new QHBoxLayout();
|
||
m_exposureSlider = new QSlider(Qt::Horizontal, exposureGroup);
|
||
m_exposureSlider->setRange(100, 100000);
|
||
m_exposureSlider->setValue(1000);
|
||
|
||
m_exposureSpinBox = new QSpinBox(exposureGroup);
|
||
m_exposureSpinBox->setRange(100, 100000);
|
||
m_exposureSpinBox->setValue(1000);
|
||
m_exposureSpinBox->setMinimumWidth(80);
|
||
|
||
exposureLayout->addWidget(m_exposureSlider, 3);
|
||
exposureLayout->addWidget(m_exposureSpinBox, 1);
|
||
exposureGroupLayout->addLayout(exposureLayout);
|
||
|
||
exposureCaptureLayout->addWidget(exposureGroup);
|
||
|
||
// 拍照参数组
|
||
QGroupBox *captureGroup = new QGroupBox("拍照参数", exposureCaptureTab);
|
||
QVBoxLayout *captureLayout = new QVBoxLayout(captureGroup);
|
||
|
||
// 保存路径
|
||
QLabel *savePathLabel = new QLabel("保存路径:", captureGroup);
|
||
captureLayout->addWidget(savePathLabel);
|
||
|
||
QHBoxLayout *pathLayout = new QHBoxLayout();
|
||
m_savePathEdit = new QLineEdit(captureGroup);
|
||
m_savePathEdit->setPlaceholderText("默认: 用户图片文件夹");
|
||
m_browseSavePathBtn = new QPushButton(QIcon(":/icons/icons/folder.png"), "浏览...", captureGroup);
|
||
m_browseSavePathBtn->setMaximumWidth(90);
|
||
m_browseSavePathBtn->setMinimumHeight(32);
|
||
m_browseSavePathBtn->setStyleSheet(
|
||
"QPushButton { background-color: #607D8B; color: white; }"
|
||
"QPushButton:hover { background-color: #546E7A; }"
|
||
"QPushButton:pressed { background-color: #455A64; }"
|
||
);
|
||
pathLayout->addWidget(m_savePathEdit);
|
||
pathLayout->addWidget(m_browseSavePathBtn);
|
||
captureLayout->addLayout(pathLayout);
|
||
|
||
// 深度图格式选择
|
||
QLabel *depthFormatLabel = new QLabel("深度图格式:", captureGroup);
|
||
captureLayout->addWidget(depthFormatLabel);
|
||
m_depthFormatCombo = new QComboBox(captureGroup);
|
||
m_depthFormatCombo->addItem("PNG (8位灰度)", "png");
|
||
m_depthFormatCombo->addItem("TIFF (16位原始)", "tiff");
|
||
m_depthFormatCombo->addItem("PNG + TIFF", "both");
|
||
m_depthFormatCombo->setCurrentIndex(2);
|
||
captureLayout->addWidget(m_depthFormatCombo);
|
||
|
||
// 点云格式选择
|
||
QLabel *pointCloudFormatLabel = new QLabel("点云格式:", captureGroup);
|
||
captureLayout->addWidget(pointCloudFormatLabel);
|
||
m_pointCloudFormatCombo = new QComboBox(captureGroup);
|
||
m_pointCloudFormatCombo->addItem("PCD (PCL标准)", "pcd");
|
||
m_pointCloudFormatCombo->addItem("PLY (Polygon)", "ply");
|
||
m_pointCloudFormatCombo->addItem("PCD + PLY", "both");
|
||
m_pointCloudFormatCombo->setCurrentIndex(2);
|
||
captureLayout->addWidget(m_pointCloudFormatCombo);
|
||
|
||
exposureCaptureLayout->addWidget(captureGroup);
|
||
exposureCaptureLayout->addStretch();
|
||
|
||
// 将第二个标签页添加到选项卡
|
||
tabWidget->addTab(exposureCaptureTab, "曝光与拍照");
|
||
|
||
// 将选项卡添加到控制面板(设置伸展因子为1,使其自适应扩展)
|
||
controlLayout->addWidget(tabWidget, 1);
|
||
|
||
// ========== 统计信息显示组 ==========
|
||
QGroupBox *statsGroup = new QGroupBox("统计信息", m_controlPanel);
|
||
QVBoxLayout *statsLayout = new QVBoxLayout(statsGroup);
|
||
|
||
m_leftFpsLabel = new QLabel("左红外: 0.0 fps", statsGroup);
|
||
m_rightFpsLabel = new QLabel("右红外: 0.0 fps", statsGroup);
|
||
m_rgbFpsLabel = new QLabel("RGB: 0.0 fps", statsGroup);
|
||
m_pointCloudFpsLabel = new QLabel("点云: 0.0 fps", statsGroup);
|
||
m_resolutionLabel = new QLabel("1224x1024", statsGroup);
|
||
m_queueLabel = new QLabel("帧数: 0", statsGroup);
|
||
|
||
// 兼容旧代码
|
||
m_depthFpsLabel = m_leftFpsLabel;
|
||
|
||
QString statsStyle = "QLabel { font-size: 9pt; padding: 2px; }";
|
||
m_leftFpsLabel->setStyleSheet(statsStyle);
|
||
m_rightFpsLabel->setStyleSheet(statsStyle);
|
||
m_rgbFpsLabel->setStyleSheet(statsStyle);
|
||
m_pointCloudFpsLabel->setStyleSheet(statsStyle);
|
||
m_resolutionLabel->setStyleSheet(statsStyle);
|
||
m_queueLabel->setStyleSheet(statsStyle);
|
||
|
||
statsLayout->addWidget(m_leftFpsLabel);
|
||
statsLayout->addWidget(m_rightFpsLabel);
|
||
statsLayout->addWidget(m_rgbFpsLabel);
|
||
statsLayout->addWidget(m_pointCloudFpsLabel);
|
||
statsLayout->addWidget(m_resolutionLabel);
|
||
statsLayout->addWidget(m_queueLabel);
|
||
|
||
controlLayout->addWidget(statsGroup);
|
||
|
||
// ========== 创建右侧面板 ==========
|
||
QWidget *rightPanel = new QWidget(contentWidget);
|
||
QVBoxLayout *rightLayout = new QVBoxLayout(rightPanel);
|
||
rightLayout->setContentsMargins(5, 5, 5, 5);
|
||
rightLayout->setSpacing(5);
|
||
|
||
// ========== 四块显示区域:2×2网格布局 ==========
|
||
QWidget *displayWidget = new QWidget(rightPanel);
|
||
QGridLayout *displayGrid = new QGridLayout(displayWidget);
|
||
displayGrid->setSpacing(5);
|
||
displayGrid->setContentsMargins(5, 5, 5, 5);
|
||
|
||
// 设置行列拉伸因子
|
||
displayGrid->setRowStretch(0, 1);
|
||
displayGrid->setRowStretch(1, 1);
|
||
displayGrid->setColumnStretch(0, 1);
|
||
displayGrid->setColumnStretch(1, 1);
|
||
|
||
// 左上:点云显示
|
||
m_pointCloudWidget = new PointCloudWidget(displayWidget);
|
||
m_pointCloudWidget->setMinimumSize(400, 300);
|
||
m_pointCloudWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||
displayGrid->addWidget(m_pointCloudWidget, 0, 0);
|
||
|
||
// 右上:左红外图像
|
||
m_leftImageDisplay = new QLabel(displayWidget);
|
||
m_leftImageDisplay->setMinimumSize(400, 300);
|
||
m_leftImageDisplay->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||
m_leftImageDisplay->setStyleSheet(
|
||
"QLabel { "
|
||
" background-color: #263238; "
|
||
" color: #B0BEC5; "
|
||
" border: 2px solid #37474F; "
|
||
" border-radius: 4px; "
|
||
" font-size: 10pt; "
|
||
"}"
|
||
);
|
||
m_leftImageDisplay->setAlignment(Qt::AlignCenter);
|
||
m_leftImageDisplay->setText("左红外\n(等待数据...)");
|
||
m_leftImageDisplay->setScaledContents(false);
|
||
displayGrid->addWidget(m_leftImageDisplay, 0, 1);
|
||
|
||
// 左下:RGB图像
|
||
m_rgbImageDisplay = new QLabel(displayWidget);
|
||
m_rgbImageDisplay->setMinimumSize(400, 300);
|
||
m_rgbImageDisplay->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||
m_rgbImageDisplay->setStyleSheet(
|
||
"QLabel { "
|
||
" background-color: #263238; "
|
||
" color: #B0BEC5; "
|
||
" border: 2px solid #37474F; "
|
||
" border-radius: 4px; "
|
||
" font-size: 10pt; "
|
||
"}"
|
||
);
|
||
m_rgbImageDisplay->setAlignment(Qt::AlignCenter);
|
||
m_rgbImageDisplay->setText("RGB彩色\n(等待数据...)");
|
||
m_rgbImageDisplay->setScaledContents(false);
|
||
displayGrid->addWidget(m_rgbImageDisplay, 1, 0);
|
||
|
||
// 右下:右红外图像
|
||
m_rightImageDisplay = new QLabel(displayWidget);
|
||
m_rightImageDisplay->setMinimumSize(400, 300);
|
||
m_rightImageDisplay->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||
m_rightImageDisplay->setStyleSheet(
|
||
"QLabel { "
|
||
" background-color: #263238; "
|
||
" color: #B0BEC5; "
|
||
" border: 2px solid #37474F; "
|
||
" border-radius: 4px; "
|
||
" font-size: 10pt; "
|
||
"}"
|
||
);
|
||
m_rightImageDisplay->setAlignment(Qt::AlignCenter);
|
||
m_rightImageDisplay->setText("右红外\n(等待数据...)");
|
||
m_rightImageDisplay->setScaledContents(false);
|
||
displayGrid->addWidget(m_rightImageDisplay, 1, 1);
|
||
|
||
// 兼容旧代码:m_imageDisplay指向左红外
|
||
m_imageDisplay = m_leftImageDisplay;
|
||
|
||
// 创建日志显示区域
|
||
QWidget *logWidget = new QWidget(m_centralWidget);
|
||
QVBoxLayout *logLayout = new QVBoxLayout(logWidget);
|
||
logLayout->setContentsMargins(5, 5, 5, 5);
|
||
logLayout->setSpacing(5);
|
||
|
||
// 日志标题和按钮
|
||
QHBoxLayout *logHeaderLayout = new QHBoxLayout();
|
||
QLabel *logTitle = new QLabel("实时日志", logWidget);
|
||
QFont titleFont = logTitle->font();
|
||
titleFont.setBold(true);
|
||
titleFont.setPointSize(11);
|
||
logTitle->setFont(titleFont);
|
||
logTitle->setStyleSheet("QLabel { padding: 4px; }"); // 移除硬编码颜色,使用系统调色板
|
||
|
||
m_clearLogBtn = new QPushButton(QIcon(":/icons/icons/clear.png"), "清除日志", logWidget);
|
||
m_clearLogBtn->setMaximumWidth(110);
|
||
m_clearLogBtn->setMinimumHeight(32);
|
||
m_clearLogBtn->setStyleSheet(
|
||
"QPushButton { background-color: #757575; color: white; font-size: 9pt; }"
|
||
"QPushButton:hover { background-color: #616161; }"
|
||
"QPushButton:pressed { background-color: #424242; }"
|
||
);
|
||
|
||
m_saveLogBtn = new QPushButton(QIcon(":/icons/icons/save.png"), "保存日志", logWidget);
|
||
m_saveLogBtn->setMaximumWidth(110);
|
||
m_saveLogBtn->setMinimumHeight(32);
|
||
m_saveLogBtn->setStyleSheet(
|
||
"QPushButton { background-color: #4CAF50; color: white; font-size: 9pt; }"
|
||
"QPushButton:hover { background-color: #45A049; }"
|
||
"QPushButton:pressed { background-color: #388E3C; }"
|
||
);
|
||
|
||
logHeaderLayout->addWidget(logTitle);
|
||
logHeaderLayout->addStretch();
|
||
logHeaderLayout->addWidget(m_clearLogBtn);
|
||
logHeaderLayout->addWidget(m_saveLogBtn);
|
||
logLayout->addLayout(logHeaderLayout);
|
||
|
||
// 日志文本显示
|
||
m_logDisplay = new QTextEdit(logWidget);
|
||
m_logDisplay->setReadOnly(true);
|
||
m_logDisplay->setMinimumHeight(100);
|
||
m_logDisplay->setStyleSheet("QTextEdit { background-color: #1E1E1E; color: #D4D4D4; font-family: Consolas, monospace; font-size: 9pt; }");
|
||
logLayout->addWidget(m_logDisplay);
|
||
|
||
// 将显示区和日志区添加到垂直分割器中
|
||
QSplitter *contentSplitter = new QSplitter(Qt::Vertical, rightPanel);
|
||
contentSplitter->addWidget(displayWidget);
|
||
contentSplitter->addWidget(logWidget);
|
||
contentSplitter->setStretchFactor(0, 3); // 显示区占3份
|
||
contentSplitter->setStretchFactor(1, 1); // 日志区占1份
|
||
|
||
rightLayout->addWidget(contentSplitter);
|
||
|
||
// 将左侧控制面板和右侧面板添加到内容布局
|
||
contentLayout->addWidget(m_controlPanel);
|
||
contentLayout->addWidget(rightPanel);
|
||
|
||
mainLayout->addWidget(contentWidget);
|
||
}
|
||
|
||
void MainWindow::setupConnections()
|
||
{
|
||
// 相机控制按钮连接
|
||
connect(m_startBtn, &QPushButton::clicked, this, &MainWindow::onStartClicked);
|
||
connect(m_stopBtn, &QPushButton::clicked, this, &MainWindow::onStopClicked);
|
||
connect(m_captureBtn, &QPushButton::clicked, this, &MainWindow::onCaptureClicked);
|
||
connect(m_browseSavePathBtn, &QPushButton::clicked, this, &MainWindow::onBrowseSavePathClicked);
|
||
|
||
// 曝光时间控制连接
|
||
connect(m_exposureSlider, &QSlider::valueChanged, m_exposureSpinBox, &QSpinBox::setValue);
|
||
connect(m_exposureSpinBox, QOverload<int>::of(&QSpinBox::valueChanged), m_exposureSlider, &QSlider::setValue);
|
||
connect(m_exposureSlider, &QSlider::valueChanged, this, &MainWindow::onExposureChanged);
|
||
|
||
// 网络配置连接
|
||
connect(m_connectBtn, &QPushButton::clicked, this, &MainWindow::onConnectClicked);
|
||
connect(m_ctrlPortSpinBox, QOverload<int>::of(&QSpinBox::valueChanged), this, &MainWindow::onControlPortChanged);
|
||
connect(m_dataPortSpinBox, QOverload<int>::of(&QSpinBox::valueChanged), this, &MainWindow::onDataPortChanged);
|
||
|
||
// 网络状态信号连接
|
||
connect(m_networkManager.get(), &NetworkManager::connected, this, &MainWindow::onNetworkConnected);
|
||
connect(m_networkManager.get(), &NetworkManager::disconnected, this, &MainWindow::onNetworkDisconnected);
|
||
connect(m_networkManager.get(), &NetworkManager::dataReceived, this, &MainWindow::onDataReceived);
|
||
|
||
// GVSP数据信号连接(从NetworkManager)
|
||
connect(m_networkManager.get(), &NetworkManager::imageReceived, this, &MainWindow::onImageReceived);
|
||
connect(m_networkManager.get(), &NetworkManager::leftImageReceived, this, &MainWindow::onLeftImageReceived);
|
||
connect(m_networkManager.get(), &NetworkManager::rightImageReceived, this, &MainWindow::onRightImageReceived);
|
||
connect(m_networkManager.get(), &NetworkManager::rgbImageReceived, this, &MainWindow::onRgbImageReceived);
|
||
connect(m_networkManager.get(), &NetworkManager::depthDataReceived, this, &MainWindow::onDepthDataReceived);
|
||
connect(m_networkManager.get(), &NetworkManager::pointCloudDataReceived, this, &MainWindow::onPointCloudDataReceived);
|
||
|
||
// 点云处理信号连接
|
||
connect(m_pointCloudProcessor.get(), &PointCloudProcessor::pointCloudReady, this, &MainWindow::onPointCloudReady);
|
||
|
||
// 刷新按钮连接
|
||
connect(m_refreshBtn, &QPushButton::clicked, this, &MainWindow::onRefreshClicked);
|
||
|
||
// 设备扫描信号连接
|
||
connect(m_deviceScanner.get(), &DeviceScanner::deviceFound, this, &MainWindow::onDeviceFound);
|
||
connect(m_deviceScanner.get(), &DeviceScanner::scanProgress, this, &MainWindow::onScanProgress);
|
||
connect(m_deviceScanner.get(), &DeviceScanner::scanFinished, this, &MainWindow::onScanFinished);
|
||
|
||
// 设备列表选择连接
|
||
connect(m_deviceList, &QListWidget::itemClicked, this, &MainWindow::onDeviceSelected);
|
||
|
||
// 日志按钮连接
|
||
connect(m_clearLogBtn, &QPushButton::clicked, this, &MainWindow::onClearLogClicked);
|
||
connect(m_saveLogBtn, &QPushButton::clicked, this, &MainWindow::onSaveLogClicked);
|
||
|
||
// 传输开关按钮连接
|
||
connect(m_leftIRToggle, &QPushButton::toggled, this, [this](bool checked) {
|
||
if(checked) {
|
||
m_leftIREnabled.storeRelaxed(1); // 标记启用
|
||
m_networkManager->sendEnableLeftIR();
|
||
qDebug() << "启用左红外传输";
|
||
} else {
|
||
m_leftIREnabled.storeRelaxed(0); // 标记禁用
|
||
m_networkManager->sendDisableLeftIR();
|
||
qDebug() << "禁用左红外传输";
|
||
// 清除显示区域,恢复默认等待界面
|
||
if(m_leftImageDisplay) {
|
||
m_leftImageDisplay->setPixmap(QPixmap());
|
||
m_leftImageDisplay->setText("左红外\n(等待数据...)");
|
||
}
|
||
}
|
||
});
|
||
|
||
connect(m_rightIRToggle, &QPushButton::toggled, this, [this](bool checked) {
|
||
if(checked) {
|
||
m_rightIREnabled.storeRelaxed(1); // 标记启用
|
||
m_networkManager->sendEnableRightIR();
|
||
qDebug() << "启用右红外传输";
|
||
} else {
|
||
m_rightIREnabled.storeRelaxed(0); // 标记禁用
|
||
m_networkManager->sendDisableRightIR();
|
||
qDebug() << "禁用右红外传输";
|
||
// 清除显示区域,恢复默认等待界面
|
||
if(m_rightImageDisplay) {
|
||
m_rightImageDisplay->setPixmap(QPixmap());
|
||
m_rightImageDisplay->setText("右红外\n(等待数据...)");
|
||
}
|
||
}
|
||
});
|
||
|
||
connect(m_rgbToggle, &QPushButton::toggled, this, [this](bool checked) {
|
||
if(checked) {
|
||
m_rgbEnabled.storeRelaxed(1); // 标记启用
|
||
m_networkManager->sendEnableRGB();
|
||
qDebug() << "启用RGB传输";
|
||
} else {
|
||
m_rgbEnabled.storeRelaxed(0); // 标记禁用
|
||
m_networkManager->sendDisableRGB();
|
||
qDebug() << "禁用RGB传输";
|
||
// 清除显示区域,恢复默认等待界面
|
||
if(m_rgbImageDisplay) {
|
||
m_rgbImageDisplay->setPixmap(QPixmap());
|
||
m_rgbImageDisplay->setText("RGB彩色\n(等待数据...)");
|
||
}
|
||
}
|
||
});
|
||
|
||
// 点云颜色开关连接
|
||
connect(m_pointCloudColorToggle, &QPushButton::toggled, this, [this](bool checked) {
|
||
if(m_pointCloudWidget) {
|
||
m_pointCloudWidget->setColorMode(checked);
|
||
qDebug() << "点云着色:" << (checked ? "开启" : "关闭");
|
||
}
|
||
});
|
||
|
||
// 单目/双目模式切换按钮连接
|
||
connect(m_monocularBtn, &QPushButton::clicked, this, [this]() {
|
||
m_monocularBtn->setChecked(true);
|
||
m_binocularBtn->setChecked(false);
|
||
m_networkManager->sendMonocularMode();
|
||
qDebug() << "切换为单目模式";
|
||
});
|
||
|
||
connect(m_binocularBtn, &QPushButton::clicked, this, [this]() {
|
||
m_monocularBtn->setChecked(false);
|
||
m_binocularBtn->setChecked(true);
|
||
m_networkManager->sendBinocularMode();
|
||
qDebug() << "切换为双目模式";
|
||
});
|
||
}
|
||
|
||
void MainWindow::loadSettings()
|
||
{
|
||
// 加载曝光时间
|
||
int exposure = m_configManager->getExposureTime();
|
||
m_exposureSlider->setValue(exposure);
|
||
m_exposureSpinBox->setValue(exposure);
|
||
|
||
// 加载保存路径
|
||
QString savePath = m_configManager->getSavePath();
|
||
m_savePathEdit->setText(savePath);
|
||
|
||
// 加载深度图格式
|
||
QString depthFormat = m_configManager->getDepthFormat();
|
||
int depthIndex = m_depthFormatCombo->findData(depthFormat);
|
||
if(depthIndex >= 0) {
|
||
m_depthFormatCombo->setCurrentIndex(depthIndex);
|
||
}
|
||
|
||
// 加载点云格式
|
||
QString pointCloudFormat = m_configManager->getPointCloudFormat();
|
||
int pcIndex = m_pointCloudFormatCombo->findData(pointCloudFormat);
|
||
if(pcIndex >= 0) {
|
||
m_pointCloudFormatCombo->setCurrentIndex(pcIndex);
|
||
}
|
||
}
|
||
|
||
void MainWindow::saveSettings()
|
||
{
|
||
// 保存曝光时间
|
||
m_configManager->setExposureTime(m_exposureSlider->value());
|
||
|
||
// 保存保存路径
|
||
m_configManager->setSavePath(m_savePathEdit->text());
|
||
|
||
// 保存深度图格式
|
||
m_configManager->setDepthFormat(m_depthFormatCombo->currentData().toString());
|
||
|
||
// 保存点云格式
|
||
m_configManager->setPointCloudFormat(m_pointCloudFormatCombo->currentData().toString());
|
||
}
|
||
|
||
// ========== 槽函数实现 ==========
|
||
void MainWindow::onStartClicked()
|
||
{
|
||
qDebug() << "启动按钮点击";
|
||
m_networkManager->sendStartCommand();
|
||
addLog("发送启动命令", "INFO");
|
||
}
|
||
|
||
void MainWindow::onStopClicked()
|
||
{
|
||
qDebug() << "停止按钮点击";
|
||
m_networkManager->sendStopCommand();
|
||
addLog("发送停止命令", "INFO");
|
||
}
|
||
|
||
void MainWindow::onExposureChanged(int value)
|
||
{
|
||
qDebug() << "曝光值改变为:" << value;
|
||
m_networkManager->sendExposureCommand(value);
|
||
addLog(QString("设置曝光时间: %1 μs").arg(value), "INFO");
|
||
}
|
||
|
||
void MainWindow::onConnectClicked()
|
||
{
|
||
if(m_isConnected) {
|
||
m_networkManager->disconnectFromCamera();
|
||
m_isConnected = false;
|
||
qDebug() << "断开相机连接";
|
||
} else {
|
||
QString ip = m_configManager->getIpAddress();
|
||
int ctrlPort = m_configManager->getControlPort();
|
||
int dataPort = m_configManager->getDataPort();
|
||
|
||
if(m_networkManager->connectToCamera(ip, ctrlPort, dataPort)) {
|
||
m_isConnected = true;
|
||
qDebug() << "连接到相机";
|
||
}
|
||
}
|
||
}
|
||
|
||
void MainWindow::onIpAddressChanged(const QString &ip)
|
||
{
|
||
qDebug() << "IP地址改变为:" << ip;
|
||
m_configManager->setIpAddress(ip);
|
||
}
|
||
|
||
void MainWindow::onControlPortChanged(int port)
|
||
{
|
||
qDebug() << "控制端口改变为:" << port;
|
||
m_configManager->setControlPort(port);
|
||
}
|
||
|
||
void MainWindow::onDataPortChanged(int port)
|
||
{
|
||
qDebug() << "数据端口改变为:" << port;
|
||
m_configManager->setDataPort(port);
|
||
}
|
||
|
||
void MainWindow::onNetworkConnected()
|
||
{
|
||
qDebug() << "网络已连接 - 更新UI";
|
||
m_isConnected = true;
|
||
m_connectBtn->setText("断开");
|
||
m_connectBtn->setStyleSheet(
|
||
"QPushButton { background-color: #F44336; color: white; }"
|
||
"QPushButton:hover { background-color: #E53935; }"
|
||
"QPushButton:pressed { background-color: #C62828; }"
|
||
);
|
||
m_statusLabel->setText("● 已连接");
|
||
m_statusLabel->setStyleSheet(
|
||
"QLabel { "
|
||
" color: #4CAF50; "
|
||
" font-weight: bold; "
|
||
" font-size: 11pt; "
|
||
" padding: 8px; "
|
||
"}"
|
||
);
|
||
|
||
QString ip = m_configManager->getIpAddress();
|
||
addLog(QString("已连接到相机: %1").arg(ip), "SUCCESS");
|
||
|
||
// 同步当前按钮状态到下位机
|
||
if(m_leftIRToggle->isChecked()) {
|
||
m_networkManager->sendEnableLeftIR();
|
||
} else {
|
||
m_networkManager->sendDisableLeftIR();
|
||
}
|
||
|
||
if(m_rightIRToggle->isChecked()) {
|
||
m_networkManager->sendEnableRightIR();
|
||
} else {
|
||
m_networkManager->sendDisableRightIR();
|
||
}
|
||
|
||
if(m_rgbToggle->isChecked()) {
|
||
m_networkManager->sendEnableRGB();
|
||
} else {
|
||
m_networkManager->sendDisableRGB();
|
||
}
|
||
|
||
addLog("已同步相机开关状态", "INFO");
|
||
}
|
||
|
||
void MainWindow::onNetworkDisconnected()
|
||
{
|
||
qDebug() << "网络已断开 - 更新UI";
|
||
m_isConnected = false;
|
||
m_connectBtn->setText("连接");
|
||
m_connectBtn->setStyleSheet(
|
||
"QPushButton { background-color: #4CAF50; color: white; }"
|
||
"QPushButton:hover { background-color: #45A049; }"
|
||
"QPushButton:pressed { background-color: #388E3C; }"
|
||
);
|
||
m_statusLabel->setText("● 未连接");
|
||
m_statusLabel->setStyleSheet(
|
||
"QLabel { "
|
||
" color: #F44336; "
|
||
" font-weight: bold; "
|
||
" font-size: 11pt; "
|
||
" padding: 8px; "
|
||
"}"
|
||
);
|
||
|
||
addLog("相机连接已断开", "WARNING");
|
||
}
|
||
|
||
void MainWindow::updateUI()
|
||
{
|
||
// 定时器更新UI
|
||
}
|
||
|
||
// ========== 设备扫描槽函数 ==========
|
||
void MainWindow::onRefreshClicked()
|
||
{
|
||
qDebug() << "刷新设备按钮点击";
|
||
m_deviceList->clear();
|
||
|
||
if(m_deviceScanner->isScanning()) {
|
||
m_deviceScanner->stopScan();
|
||
m_refreshBtn->setText("刷新设备");
|
||
} else {
|
||
m_deviceScanner->startScan();
|
||
m_refreshBtn->setText("停止扫描");
|
||
}
|
||
}
|
||
|
||
void MainWindow::onDeviceFound(const DeviceInfo &device)
|
||
{
|
||
qDebug() << "发现设备:" << device.ipAddress;
|
||
|
||
// 添加到列表
|
||
QString itemText = QString("%1 - %2").arg(device.ipAddress).arg(device.deviceName);
|
||
QListWidgetItem *item = new QListWidgetItem(itemText);
|
||
item->setData(Qt::UserRole, device.ipAddress);
|
||
m_deviceList->addItem(item);
|
||
}
|
||
|
||
void MainWindow::onScanProgress(int current, int total)
|
||
{
|
||
qDebug() << "扫描进度:" << current << "/" << total;
|
||
}
|
||
|
||
void MainWindow::onScanFinished(int devicesFound)
|
||
{
|
||
qDebug() << "扫描完成。发现" << devicesFound << "个设备";
|
||
m_refreshBtn->setText("刷新设备");
|
||
}
|
||
|
||
void MainWindow::onDeviceSelected(QListWidgetItem *item)
|
||
{
|
||
QString ip = item->data(Qt::UserRole).toString();
|
||
qDebug() << "选择设备:" << ip;
|
||
|
||
// 更新配置管理器中的IP地址
|
||
m_configManager->setIpAddress(ip);
|
||
}
|
||
|
||
// ========== 数据接收槽函数 ==========
|
||
void MainWindow::onDataReceived(const QByteArray &data)
|
||
{
|
||
// NetworkManager已经自动将数据传递给GVSP解析器
|
||
// 这里只用于调试统计
|
||
Q_UNUSED(data);
|
||
}
|
||
|
||
// ========== GVSP数据处理槽函数 ==========
|
||
void MainWindow::onImageReceived(const QImage &image, uint32_t blockId)
|
||
{
|
||
// 保存当前图像用于拍照
|
||
m_currentImage = image;
|
||
m_currentFrameId = blockId;
|
||
|
||
// 计算深度图FPS和累计帧数
|
||
m_depthFrameCount++;
|
||
m_totalDepthFrameCount++;
|
||
QDateTime currentTime = QDateTime::currentDateTime();
|
||
if(m_lastDepthFrameTime.isValid()) {
|
||
qint64 elapsed = m_lastDepthFrameTime.msecsTo(currentTime);
|
||
if(elapsed >= 1000) { // 每秒更新一次FPS
|
||
m_currentDepthFps = (m_depthFrameCount * 1000.0) / elapsed;
|
||
m_depthFrameCount = 0;
|
||
m_lastDepthFrameTime = currentTime;
|
||
updateStatistics();
|
||
}
|
||
} else {
|
||
m_lastDepthFrameTime = currentTime;
|
||
}
|
||
|
||
// 将图像显示在UI上(使用快速缩放)
|
||
if(m_imageDisplay) {
|
||
QPixmap pixmap = QPixmap::fromImage(image);
|
||
m_imageDisplay->setPixmap(pixmap.scaled(m_imageDisplay->size(), Qt::KeepAspectRatio, Qt::FastTransformation));
|
||
}
|
||
}
|
||
|
||
void MainWindow::onLeftImageReceived(const QByteArray &jpegData, uint32_t blockId)
|
||
{
|
||
// 检查左红外是否启用,如果禁用则忽略数据(防止关闭后闪烁)
|
||
if(m_leftIREnabled.loadAcquire() == 0) {
|
||
return;
|
||
}
|
||
|
||
// 保存原始数据(用于拍照保存完整分辨率)
|
||
m_currentLeftIRData = jpegData;
|
||
|
||
// 计算左红外FPS
|
||
m_leftFrameCount++;
|
||
m_totalLeftFrameCount++;
|
||
QDateTime currentTime = QDateTime::currentDateTime();
|
||
if(m_lastLeftFrameTime.isValid()) {
|
||
qint64 elapsed = m_lastLeftFrameTime.msecsTo(currentTime);
|
||
if(elapsed >= 1000) {
|
||
m_currentLeftFps = (m_leftFrameCount * 1000.0) / elapsed;
|
||
m_leftFrameCount = 0;
|
||
m_lastLeftFrameTime = currentTime;
|
||
updateStatistics();
|
||
}
|
||
} else {
|
||
m_lastLeftFrameTime = currentTime;
|
||
}
|
||
|
||
// 使用后台线程处理红外数据,避免阻塞UI
|
||
if(m_leftImageDisplay && jpegData.size() > 0) {
|
||
// 检查数据大小:8位下采样(612x512)或16位原始(1224x1024)
|
||
size_t size8bit = 612 * 512;
|
||
size_t size16bit = 1224 * 1024 * sizeof(uint16_t);
|
||
|
||
if(jpegData.size() == size8bit) {
|
||
// 8位下采样格式:直接显示
|
||
QByteArray dataCopy = jpegData;
|
||
QtConcurrent::run([this, dataCopy]() {
|
||
try {
|
||
QImage image(reinterpret_cast<const uchar*>(dataCopy.constData()),
|
||
612, 512, 612, QImage::Format_Grayscale8);
|
||
QImage imageCopy = image.copy();
|
||
|
||
QMetaObject::invokeMethod(this, [this, imageCopy]() {
|
||
if(m_leftImageDisplay) {
|
||
QPixmap pixmap = QPixmap::fromImage(imageCopy);
|
||
m_leftImageDisplay->setPixmap(pixmap.scaled(
|
||
m_leftImageDisplay->size(), Qt::KeepAspectRatio, Qt::FastTransformation));
|
||
}
|
||
}, Qt::QueuedConnection);
|
||
} catch (const std::exception &e) {
|
||
qDebug() << "[MainWindow] ERROR: Left IR 8bit processing exception:" << e.what();
|
||
}
|
||
});
|
||
} else if(jpegData.size() == size16bit) {
|
||
// 16位原始格式:需要归一化处理
|
||
QByteArray dataCopy = jpegData;
|
||
|
||
// 在后台线程处理
|
||
QtConcurrent::run([this, dataCopy]() {
|
||
try {
|
||
const uint16_t* src = reinterpret_cast<const uint16_t*>(dataCopy.constData());
|
||
|
||
// 方案2:快速百分位数估算(无需排序,采样估算)
|
||
// 优点:适应不同环境,画面对比度好,速度快10倍以上
|
||
uint16_t minVal = 65535, maxVal = 0;
|
||
|
||
// 第一遍:快速扫描找到粗略范围(每隔8个像素采样)
|
||
for (int i = 0; i < 1224 * 1024; i += 8) {
|
||
uint16_t val = src[i];
|
||
if(val > 0) {
|
||
if(val < minVal) minVal = val;
|
||
if(val > maxVal) maxVal = val;
|
||
}
|
||
}
|
||
|
||
// 第二遍:使用直方图统计精确百分位数(避免排序)
|
||
if(maxVal > minVal) {
|
||
const int histSize = 256;
|
||
int histogram[histSize] = {0};
|
||
float binWidth = (maxVal - minVal) / (float)histSize;
|
||
|
||
// 构建直方图
|
||
for (int i = 0; i < 1224 * 1024; i++) {
|
||
if(src[i] > 0) {
|
||
int bin = (src[i] - minVal) / binWidth;
|
||
if(bin >= histSize) bin = histSize - 1;
|
||
histogram[bin]++;
|
||
}
|
||
}
|
||
|
||
// 计算1%和99%百分位数
|
||
int totalPixels = 0;
|
||
for (int i = 0; i < histSize; i++) totalPixels += histogram[i];
|
||
|
||
int thresh_1 = totalPixels * 0.01;
|
||
int thresh_99 = totalPixels * 0.99;
|
||
|
||
int cumsum = 0;
|
||
for (int i = 0; i < histSize; i++) {
|
||
cumsum += histogram[i];
|
||
if(cumsum >= thresh_1 && minVal == 65535) {
|
||
minVal = minVal + i * binWidth;
|
||
}
|
||
if(cumsum >= thresh_99) {
|
||
maxVal = minVal + i * binWidth;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 创建8位图像并归一化
|
||
QImage image(1224, 1024, QImage::Format_Grayscale8);
|
||
uint8_t* dst = image.bits();
|
||
float scale = (maxVal > minVal) ? (255.0f / (maxVal - minVal)) : 0.0f;
|
||
|
||
for (int i = 0; i < 1224 * 1024; i++) {
|
||
if(src[i] == 0) {
|
||
dst[i] = 0;
|
||
} else if(src[i] <= minVal) {
|
||
dst[i] = 0;
|
||
} else if(src[i] >= maxVal) {
|
||
dst[i] = 255;
|
||
} else {
|
||
dst[i] = static_cast<uint8_t>((src[i] - minVal) * scale);
|
||
}
|
||
}
|
||
|
||
QImage imageCopy = image.copy();
|
||
|
||
// 在主线程更新UI
|
||
QMetaObject::invokeMethod(this, [this, imageCopy]() {
|
||
if(m_leftImageDisplay) {
|
||
QPixmap pixmap = QPixmap::fromImage(imageCopy);
|
||
m_leftImageDisplay->setPixmap(pixmap.scaled(
|
||
m_leftImageDisplay->size(), Qt::KeepAspectRatio, Qt::FastTransformation));
|
||
}
|
||
}, Qt::QueuedConnection);
|
||
} catch (const std::exception &e) {
|
||
qDebug() << "[MainWindow] ERROR: Left IR processing exception:" << e.what();
|
||
}
|
||
});
|
||
} else {
|
||
qDebug() << "[MainWindow] ERROR: Left IR data size mismatch:" << jpegData.size()
|
||
<< "(expected 8bit:" << size8bit << "or 16bit:" << size16bit << ")";
|
||
}
|
||
}
|
||
}
|
||
|
||
void MainWindow::onRightImageReceived(const QByteArray &jpegData, uint32_t blockId)
|
||
{
|
||
// 检查右红外是否启用,如果禁用则忽略数据(防止关闭后闪烁)
|
||
if(m_rightIREnabled.loadAcquire() == 0) {
|
||
return;
|
||
}
|
||
|
||
// 保存原始数据(用于拍照保存完整分辨率)
|
||
m_currentRightIRData = jpegData;
|
||
|
||
// 计算右红外FPS
|
||
m_rightFrameCount++;
|
||
m_totalRightFrameCount++;
|
||
QDateTime currentTime = QDateTime::currentDateTime();
|
||
if(m_lastRightFrameTime.isValid()) {
|
||
qint64 elapsed = m_lastRightFrameTime.msecsTo(currentTime);
|
||
if(elapsed >= 1000) {
|
||
m_currentRightFps = (m_rightFrameCount * 1000.0) / elapsed;
|
||
m_rightFrameCount = 0;
|
||
m_lastRightFrameTime = currentTime;
|
||
updateStatistics();
|
||
}
|
||
} else {
|
||
m_lastRightFrameTime = currentTime;
|
||
}
|
||
|
||
// 使用后台线程处理红外数据,避免阻塞UI
|
||
if(m_rightImageDisplay && jpegData.size() > 0) {
|
||
// 检查数据大小:8位下采样(612x512)或16位原始(1224x1024)
|
||
size_t size8bit = 612 * 512;
|
||
size_t size16bit = 1224 * 1024 * sizeof(uint16_t);
|
||
|
||
if(jpegData.size() == size8bit) {
|
||
// 8位下采样格式:直接显示
|
||
QByteArray dataCopy = jpegData;
|
||
QtConcurrent::run([this, dataCopy]() {
|
||
try {
|
||
QImage image(reinterpret_cast<const uchar*>(dataCopy.constData()),
|
||
612, 512, 612, QImage::Format_Grayscale8);
|
||
QImage imageCopy = image.copy();
|
||
|
||
QMetaObject::invokeMethod(this, [this, imageCopy]() {
|
||
if(m_rightImageDisplay) {
|
||
QPixmap pixmap = QPixmap::fromImage(imageCopy);
|
||
m_rightImageDisplay->setPixmap(pixmap.scaled(
|
||
m_rightImageDisplay->size(), Qt::KeepAspectRatio, Qt::FastTransformation));
|
||
}
|
||
}, Qt::QueuedConnection);
|
||
} catch (const std::exception &e) {
|
||
qDebug() << "[MainWindow] ERROR: Right IR 8bit processing exception:" << e.what();
|
||
}
|
||
});
|
||
} else if(jpegData.size() == size16bit) {
|
||
// 16位原始格式:需要归一化处理
|
||
QByteArray dataCopy = jpegData;
|
||
|
||
// 在后台线程处理
|
||
QtConcurrent::run([this, dataCopy]() {
|
||
try {
|
||
const uint16_t* src = reinterpret_cast<const uint16_t*>(dataCopy.constData());
|
||
|
||
// 方案2:快速百分位数估算(无需排序,采样估算)
|
||
// 优点:适应不同环境,画面对比度好,速度快10倍以上
|
||
uint16_t minVal = 65535, maxVal = 0;
|
||
|
||
// 第一遍:快速扫描找到粗略范围(每隔8个像素采样)
|
||
for (int i = 0; i < 1224 * 1024; i += 8) {
|
||
uint16_t val = src[i];
|
||
if(val > 0) {
|
||
if(val < minVal) minVal = val;
|
||
if(val > maxVal) maxVal = val;
|
||
}
|
||
}
|
||
|
||
// 第二遍:使用直方图统计精确百分位数(避免排序)
|
||
if(maxVal > minVal) {
|
||
const int histSize = 256;
|
||
int histogram[histSize] = {0};
|
||
float binWidth = (maxVal - minVal) / (float)histSize;
|
||
|
||
// 构建直方图
|
||
for (int i = 0; i < 1224 * 1024; i++) {
|
||
if(src[i] > 0) {
|
||
int bin = (src[i] - minVal) / binWidth;
|
||
if(bin >= histSize) bin = histSize - 1;
|
||
histogram[bin]++;
|
||
}
|
||
}
|
||
|
||
// 计算1%和99%百分位数
|
||
int totalPixels = 0;
|
||
for (int i = 0; i < histSize; i++) totalPixels += histogram[i];
|
||
|
||
int thresh_1 = totalPixels * 0.01;
|
||
int thresh_99 = totalPixels * 0.99;
|
||
|
||
int cumsum = 0;
|
||
for (int i = 0; i < histSize; i++) {
|
||
cumsum += histogram[i];
|
||
if(cumsum >= thresh_1 && minVal == 65535) {
|
||
minVal = minVal + i * binWidth;
|
||
}
|
||
if(cumsum >= thresh_99) {
|
||
maxVal = minVal + i * binWidth;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 创建8位图像并归一化
|
||
QImage image(1224, 1024, QImage::Format_Grayscale8);
|
||
uint8_t* dst = image.bits();
|
||
float scale = (maxVal > minVal) ? (255.0f / (maxVal - minVal)) : 0.0f;
|
||
|
||
for (int i = 0; i < 1224 * 1024; i++) {
|
||
if(src[i] == 0) {
|
||
dst[i] = 0;
|
||
} else if(src[i] <= minVal) {
|
||
dst[i] = 0;
|
||
} else if(src[i] >= maxVal) {
|
||
dst[i] = 255;
|
||
} else {
|
||
dst[i] = static_cast<uint8_t>((src[i] - minVal) * scale);
|
||
}
|
||
}
|
||
|
||
QImage imageCopy = image.copy();
|
||
|
||
// 在主线程更新UI
|
||
QMetaObject::invokeMethod(this, [this, imageCopy]() {
|
||
if(m_rightImageDisplay) {
|
||
QPixmap pixmap = QPixmap::fromImage(imageCopy);
|
||
m_rightImageDisplay->setPixmap(pixmap.scaled(
|
||
m_rightImageDisplay->size(), Qt::KeepAspectRatio, Qt::FastTransformation));
|
||
}
|
||
}, Qt::QueuedConnection);
|
||
} catch (const std::exception &e) {
|
||
qDebug() << "[MainWindow] ERROR: Right IR processing exception:" << e.what();
|
||
}
|
||
});
|
||
} else {
|
||
qDebug() << "[MainWindow] ERROR: Right IR data size mismatch:" << jpegData.size()
|
||
<< "(expected 8bit:" << size8bit << "or 16bit:" << size16bit << ")";
|
||
}
|
||
}
|
||
}
|
||
|
||
void MainWindow::onRgbImageReceived(const QByteArray &jpegData, uint32_t blockId)
|
||
{
|
||
// 检查RGB是否启用,如果禁用则忽略数据(防止关闭后闪烁)
|
||
if(m_rgbEnabled.loadAcquire() == 0) {
|
||
return;
|
||
}
|
||
|
||
// 记录接收时间
|
||
static QDateTime lastLogTime = QDateTime::currentDateTime();
|
||
QDateTime receiveTime = QDateTime::currentDateTime();
|
||
|
||
// 保存原始JPEG数据(用于拍照保存完整分辨率)
|
||
m_currentRgbData = jpegData;
|
||
|
||
// 计算RGB FPS
|
||
m_rgbFrameCount++;
|
||
m_totalRgbFrameCount++;
|
||
QDateTime currentTime = QDateTime::currentDateTime();
|
||
if(m_lastRgbFrameTime.isValid()) {
|
||
qint64 elapsed = m_lastRgbFrameTime.msecsTo(currentTime);
|
||
if(elapsed >= 1000) {
|
||
m_currentRgbFps = (m_rgbFrameCount * 1000.0) / elapsed;
|
||
m_rgbFrameCount = 0;
|
||
m_lastRgbFrameTime = currentTime;
|
||
updateStatistics();
|
||
}
|
||
} else {
|
||
m_lastRgbFrameTime = currentTime;
|
||
}
|
||
|
||
// 移除频繁的日志输出
|
||
|
||
// 使用后台线程解码MJPEG,避免阻塞UI
|
||
if(m_rgbImageDisplay && jpegData.size() > 0) {
|
||
// 智能帧丢弃:如果上一帧还在处理,跳过当前帧(避免积压旧帧)
|
||
if(m_rgbProcessing.loadAcquire() > 0) {
|
||
return; // 跳过当前帧,优先处理最新帧
|
||
}
|
||
|
||
// 标记开始处理
|
||
m_rgbProcessing.ref();
|
||
|
||
// 复制数据到局部变量
|
||
QByteArray dataCopy = jpegData;
|
||
|
||
// 在后台线程解码
|
||
QtConcurrent::run([this, dataCopy]() {
|
||
try {
|
||
// 使用OpenCV解码
|
||
std::vector<uchar> buffer(dataCopy.begin(), dataCopy.end());
|
||
cv::Mat cvImage = cv::imdecode(buffer, cv::IMREAD_COLOR);
|
||
|
||
if(!cvImage.empty()) {
|
||
// 缩放到1/2大小
|
||
cv::Mat resized;
|
||
cv::resize(cvImage, resized, cv::Size(), 0.5, 0.5, cv::INTER_LINEAR);
|
||
|
||
// 转换BGR到RGB
|
||
cv::cvtColor(resized, resized, cv::COLOR_BGR2RGB);
|
||
|
||
// 转换cv::Mat到QImage
|
||
QImage tempImage(resized.data, resized.cols, resized.rows,
|
||
resized.step, QImage::Format_RGB888);
|
||
QImage image = tempImage.copy();
|
||
|
||
// 在后台线程完成缩放,避免主线程阻塞
|
||
int displayWidth = m_rgbImageDisplay->width();
|
||
int displayHeight = m_rgbImageDisplay->height();
|
||
|
||
// 计算保持宽高比的缩放尺寸
|
||
QSize scaledSize = image.size().scaled(displayWidth, displayHeight, Qt::KeepAspectRatio);
|
||
QImage scaledImage = image.scaled(scaledSize, Qt::KeepAspectRatio, Qt::FastTransformation);
|
||
QPixmap pixmap = QPixmap::fromImage(scaledImage);
|
||
|
||
// 使用DirectConnection直接在主线程更新UI(更快,避免队列积压)
|
||
QMetaObject::invokeMethod(this, [this, pixmap]() {
|
||
if(m_rgbImageDisplay) {
|
||
m_rgbImageDisplay->setPixmap(pixmap);
|
||
}
|
||
}, Qt::QueuedConnection);
|
||
}
|
||
} catch (const std::exception &e) {
|
||
qDebug() << "[MainWindow] ERROR: RGB decode exception:" << e.what();
|
||
}
|
||
|
||
// 处理完成,减少标志
|
||
m_rgbProcessing.deref();
|
||
});
|
||
}
|
||
}
|
||
|
||
void MainWindow::onDepthDataReceived(const QByteArray &depthData, uint32_t blockId)
|
||
{
|
||
// 实时处理每一帧
|
||
// 注释掉频繁的日志输出
|
||
// qDebug() << "Depth data received: Block" << blockId << "Size:" << depthData.size() << "bytes";
|
||
|
||
// 调用PointCloudProcessor进行OpenCL计算
|
||
m_pointCloudProcessor->processDepthData(depthData, blockId);
|
||
}
|
||
|
||
void MainWindow::onPointCloudDataReceived(const QByteArray &cloudData, uint32_t blockId)
|
||
{
|
||
// qDebug() << "[MainWindow] Point cloud data received: Block" << blockId << "Size:" << cloudData.size() << "bytes";
|
||
|
||
// 使用QtConcurrent在后台线程处理点云数据
|
||
QtConcurrent::run([this, cloudData, blockId]() {
|
||
m_pointCloudProcessor->processPointCloudData(cloudData, blockId);
|
||
});
|
||
}
|
||
|
||
void MainWindow::onPointCloudReady(pcl::PointCloud<pcl::PointXYZ>::Ptr cloud, uint32_t blockId)
|
||
{
|
||
// 注释掉频繁的日志输出
|
||
// qDebug() << "Point cloud ready: Block" << blockId << "Points:" << cloud->size();
|
||
|
||
// 保存当前点云用于拍照
|
||
m_currentPointCloud = cloud;
|
||
|
||
// 计算点云FPS和累计帧数
|
||
m_pointCloudFrameCount++;
|
||
m_totalPointCloudFrameCount++;
|
||
QDateTime currentTime = QDateTime::currentDateTime();
|
||
if(m_lastPointCloudFrameTime.isValid()) {
|
||
qint64 elapsed = m_lastPointCloudFrameTime.msecsTo(currentTime);
|
||
if(elapsed >= 1000) { // 每秒更新一次FPS
|
||
m_currentPointCloudFps = (m_pointCloudFrameCount * 1000.0) / elapsed;
|
||
m_pointCloudFrameCount = 0;
|
||
m_lastPointCloudFrameTime = currentTime;
|
||
updateStatistics();
|
||
}
|
||
} else {
|
||
m_lastPointCloudFrameTime = currentTime;
|
||
}
|
||
|
||
// 更新点云显示
|
||
if(m_pointCloudWidget) {
|
||
m_pointCloudWidget->updatePointCloud(cloud);
|
||
}
|
||
}
|
||
|
||
// ========== 拍照功能 ==========
|
||
void MainWindow::onCaptureClicked()
|
||
{
|
||
qDebug() << "拍照按钮点击";
|
||
|
||
// 检查是否有可用数据
|
||
if(m_currentImage.isNull() && (!m_currentPointCloud || m_currentPointCloud->empty())) {
|
||
QMessageBox::warning(this, "拍照失败", "没有可用的图像或点云数据。\n请先启动相机采集。");
|
||
addLog("拍照失败:没有可用数据", "ERROR");
|
||
return;
|
||
}
|
||
|
||
// 获取保存路径
|
||
QString saveDir = m_savePathEdit->text().trimmed();
|
||
if(saveDir.isEmpty()) {
|
||
// 使用默认路径:用户图片文件夹
|
||
saveDir = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
|
||
qDebug() << "使用默认保存路径:" << saveDir;
|
||
}
|
||
|
||
// 检查目录是否存在,不存在则创建
|
||
QDir dir(saveDir);
|
||
if(!dir.exists()) {
|
||
if(!dir.mkpath(".")) {
|
||
QMessageBox::warning(this, "拍照失败", QString("无法创建保存目录:\n%1").arg(saveDir));
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 生成时间戳文件名
|
||
QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss");
|
||
QString frameId = QString::number(m_currentFrameId);
|
||
QString baseName = QString("capture_%1_frame%2").arg(timestamp).arg(frameId);
|
||
|
||
// 获取格式选择
|
||
QString depthFormat = m_depthFormatCombo->currentData().toString();
|
||
QString pointCloudFormat = m_pointCloudFormatCombo->currentData().toString();
|
||
|
||
// 复制当前数据(避免在后台线程中访问成员变量)
|
||
QImage imageCopy = m_currentImage.copy();
|
||
pcl::PointCloud<pcl::PointXYZ>::Ptr cloudCopy(new pcl::PointCloud<pcl::PointXYZ>(*m_currentPointCloud));
|
||
|
||
// 复制三个相机的原始JPEG数据
|
||
QByteArray leftIRCopy = m_currentLeftIRData;
|
||
QByteArray rightIRCopy = m_currentRightIRData;
|
||
QByteArray rgbCopy = m_currentRgbData;
|
||
|
||
qDebug() << "开始后台保存...";
|
||
addLog(QString("开始保存: %1").arg(baseName), "INFO");
|
||
|
||
// 在后台线程中执行保存操作
|
||
QtConcurrent::run([this, saveDir, baseName, depthFormat, pointCloudFormat,
|
||
imageCopy, cloudCopy, leftIRCopy, rightIRCopy, rgbCopy]() {
|
||
this->performBackgroundSave(saveDir, baseName, depthFormat, pointCloudFormat,
|
||
imageCopy, cloudCopy, leftIRCopy, rightIRCopy, rgbCopy);
|
||
});
|
||
}
|
||
|
||
void MainWindow::onBrowseSavePathClicked()
|
||
{
|
||
QString dir = QFileDialog::getExistingDirectory(this, "选择保存目录",
|
||
m_savePathEdit->text().isEmpty()
|
||
? QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)
|
||
: m_savePathEdit->text(),
|
||
QFileDialog::ShowDirsOnly);
|
||
if(!dir.isEmpty()) {
|
||
m_savePathEdit->setText(dir);
|
||
qDebug() << "设置保存路径:" << dir;
|
||
}
|
||
}
|
||
|
||
void MainWindow::performBackgroundSave(const QString &saveDir, const QString &baseName,
|
||
const QString &depthFormat, const QString &pointCloudFormat,
|
||
const QImage &image, pcl::PointCloud<pcl::PointXYZ>::Ptr cloud,
|
||
const QByteArray &leftIRData, const QByteArray &rightIRData,
|
||
const QByteArray &rgbData)
|
||
{
|
||
qDebug() << "后台保存线程开始...";
|
||
|
||
bool depthSuccess = false;
|
||
bool cloudSuccess = false;
|
||
|
||
// 保存深度图
|
||
if(!image.isNull()) {
|
||
try {
|
||
// 转换QImage到OpenCV Mat
|
||
cv::Mat mat;
|
||
if(image.format() == QImage::Format_Grayscale8) {
|
||
mat = cv::Mat(image.height(), image.width(), CV_8UC1,
|
||
const_cast<uchar*>(image.bits()), image.bytesPerLine()).clone();
|
||
} else if(image.format() == QImage::Format_Grayscale16) {
|
||
mat = cv::Mat(image.height(), image.width(), CV_16UC1,
|
||
const_cast<uchar*>(image.bits()), image.bytesPerLine()).clone();
|
||
} else {
|
||
// 转换为灰度图
|
||
QImage grayImage = image.convertToFormat(QImage::Format_Grayscale8);
|
||
mat = cv::Mat(grayImage.height(), grayImage.width(), CV_8UC1,
|
||
const_cast<uchar*>(grayImage.bits()), grayImage.bytesPerLine()).clone();
|
||
}
|
||
|
||
// 根据format参数保存
|
||
if(depthFormat == "png" || depthFormat == "both") {
|
||
QString pngPath = QString("%1/%2_depth.png").arg(saveDir).arg(baseName);
|
||
cv::Mat mat8bit;
|
||
if(mat.type() == CV_16UC1) {
|
||
mat.convertTo(mat8bit, CV_8UC1, 255.0 / 65535.0);
|
||
} else {
|
||
mat8bit = mat;
|
||
}
|
||
cv::imwrite(pngPath.toStdString(), mat8bit);
|
||
qDebug() << "保存PNG深度图:" << pngPath;
|
||
depthSuccess = true;
|
||
}
|
||
|
||
if(depthFormat == "tiff" || depthFormat == "both") {
|
||
QString tiffPath = QString("%1/%2_depth.tiff").arg(saveDir).arg(baseName);
|
||
if(mat.type() == CV_16UC1) {
|
||
cv::imwrite(tiffPath.toStdString(), mat);
|
||
qDebug() << "保存TIFF深度图(16位):" << tiffPath;
|
||
} else {
|
||
cv::Mat mat16bit;
|
||
mat.convertTo(mat16bit, CV_16UC1, 257.0);
|
||
cv::imwrite(tiffPath.toStdString(), mat16bit);
|
||
qDebug() << "保存TIFF深度图(从8位转换):" << tiffPath;
|
||
}
|
||
depthSuccess = true;
|
||
}
|
||
} catch (const std::exception &e) {
|
||
qDebug() << "保存深度图失败:" << e.what();
|
||
}
|
||
}
|
||
|
||
// 保存点云
|
||
if(cloud && !cloud->empty()) {
|
||
try {
|
||
if(pointCloudFormat == "pcd" || pointCloudFormat == "both") {
|
||
QString pcdPath = QString("%1/%2_pointcloud.pcd").arg(saveDir).arg(baseName);
|
||
if(pcl::io::savePCDFileBinary(pcdPath.toStdString(), *cloud) == 0) {
|
||
qDebug() << "保存PCD点云:" << pcdPath;
|
||
cloudSuccess = true;
|
||
} else {
|
||
qDebug() << "保存PCD失败";
|
||
}
|
||
}
|
||
|
||
if(pointCloudFormat == "ply" || pointCloudFormat == "both") {
|
||
QString plyPath = QString("%1/%2_pointcloud.ply").arg(saveDir).arg(baseName);
|
||
if(pcl::io::savePLYFileASCII(plyPath.toStdString(), *cloud) == 0) {
|
||
qDebug() << "保存PLY点云:" << plyPath;
|
||
cloudSuccess = true;
|
||
} else {
|
||
qDebug() << "保存PLY失败";
|
||
}
|
||
}
|
||
} catch (const std::exception &e) {
|
||
qDebug() << "保存点云失败:" << e.what();
|
||
}
|
||
}
|
||
|
||
// 保存三个相机的原始图像(完整分辨率)
|
||
bool leftIRSuccess = false;
|
||
bool rightIRSuccess = false;
|
||
bool rgbSuccess = false;
|
||
|
||
// 保存左红外图像(支持16位原始1224×1024或8位下采样612×512)
|
||
if(!leftIRData.isEmpty()) {
|
||
try {
|
||
size_t size16bit = 1224 * 1024 * sizeof(uint16_t);
|
||
size_t size8bit = 612 * 512;
|
||
|
||
if(leftIRData.size() == size16bit) {
|
||
const uint16_t* src = reinterpret_cast<const uint16_t*>(leftIRData.constData());
|
||
cv::Mat leftIR16(1024, 1224, CV_16UC1);
|
||
memcpy(leftIR16.data, src, size16bit);
|
||
|
||
if(depthFormat == "png" || depthFormat == "both") {
|
||
QString pngPath = QString("%1/%2_left_ir.png").arg(saveDir).arg(baseName);
|
||
cv::Mat leftIR8;
|
||
leftIR16.convertTo(leftIR8, CV_8UC1, 255.0 / 65535.0);
|
||
cv::imwrite(pngPath.toStdString(), leftIR8);
|
||
qDebug() << "保存左红外PNG图像(16bit):" << pngPath;
|
||
leftIRSuccess = true;
|
||
}
|
||
if(depthFormat == "tiff" || depthFormat == "both") {
|
||
QString tiffPath = QString("%1/%2_left_ir.tiff").arg(saveDir).arg(baseName);
|
||
cv::imwrite(tiffPath.toStdString(), leftIR16);
|
||
qDebug() << "保存左红外TIFF图像(16位):" << tiffPath;
|
||
leftIRSuccess = true;
|
||
}
|
||
} else if(leftIRData.size() == size8bit) {
|
||
cv::Mat leftIR8(512, 612, CV_8UC1, const_cast<char*>(leftIRData.constData()));
|
||
cv::Mat leftIR8Clone = leftIR8.clone();
|
||
|
||
if(depthFormat == "png" || depthFormat == "both") {
|
||
QString pngPath = QString("%1/%2_left_ir.png").arg(saveDir).arg(baseName);
|
||
cv::imwrite(pngPath.toStdString(), leftIR8Clone);
|
||
qDebug() << "保存左红外PNG图像(8bit下采样):" << pngPath;
|
||
leftIRSuccess = true;
|
||
}
|
||
if(depthFormat == "tiff" || depthFormat == "both") {
|
||
QString tiffPath = QString("%1/%2_left_ir.tiff").arg(saveDir).arg(baseName);
|
||
cv::imwrite(tiffPath.toStdString(), leftIR8Clone);
|
||
qDebug() << "保存左红外TIFF图像(8bit下采样):" << tiffPath;
|
||
leftIRSuccess = true;
|
||
}
|
||
} else {
|
||
qDebug() << "左红外数据大小不匹配:" << leftIRData.size()
|
||
<< "(期望16bit:" << size16bit << "或8bit:" << size8bit << ")";
|
||
}
|
||
} catch (const std::exception &e) {
|
||
qDebug() << "保存左红外图像失败:" << e.what();
|
||
}
|
||
}
|
||
|
||
// 保存右红外图像(支持16位原始1224×1024或8位下采样612×512)
|
||
if(!rightIRData.isEmpty()) {
|
||
try {
|
||
size_t size16bit = 1224 * 1024 * sizeof(uint16_t);
|
||
size_t size8bit = 612 * 512;
|
||
|
||
if(rightIRData.size() == size16bit) {
|
||
const uint16_t* src = reinterpret_cast<const uint16_t*>(rightIRData.constData());
|
||
cv::Mat rightIR16(1024, 1224, CV_16UC1);
|
||
memcpy(rightIR16.data, src, size16bit);
|
||
|
||
if(depthFormat == "png" || depthFormat == "both") {
|
||
QString pngPath = QString("%1/%2_right_ir.png").arg(saveDir).arg(baseName);
|
||
cv::Mat rightIR8;
|
||
rightIR16.convertTo(rightIR8, CV_8UC1, 255.0 / 65535.0);
|
||
cv::imwrite(pngPath.toStdString(), rightIR8);
|
||
qDebug() << "保存右红外PNG图像(16bit):" << pngPath;
|
||
rightIRSuccess = true;
|
||
}
|
||
if(depthFormat == "tiff" || depthFormat == "both") {
|
||
QString tiffPath = QString("%1/%2_right_ir.tiff").arg(saveDir).arg(baseName);
|
||
cv::imwrite(tiffPath.toStdString(), rightIR16);
|
||
qDebug() << "保存右红外TIFF图像(16位):" << tiffPath;
|
||
rightIRSuccess = true;
|
||
}
|
||
} else if(rightIRData.size() == size8bit) {
|
||
cv::Mat rightIR8(512, 612, CV_8UC1, const_cast<char*>(rightIRData.constData()));
|
||
cv::Mat rightIR8Clone = rightIR8.clone();
|
||
|
||
if(depthFormat == "png" || depthFormat == "both") {
|
||
QString pngPath = QString("%1/%2_right_ir.png").arg(saveDir).arg(baseName);
|
||
cv::imwrite(pngPath.toStdString(), rightIR8Clone);
|
||
qDebug() << "保存右红外PNG图像(8bit下采样):" << pngPath;
|
||
rightIRSuccess = true;
|
||
}
|
||
if(depthFormat == "tiff" || depthFormat == "both") {
|
||
QString tiffPath = QString("%1/%2_right_ir.tiff").arg(saveDir).arg(baseName);
|
||
cv::imwrite(tiffPath.toStdString(), rightIR8Clone);
|
||
qDebug() << "保存右红外TIFF图像(8bit下采样):" << tiffPath;
|
||
rightIRSuccess = true;
|
||
}
|
||
} else {
|
||
qDebug() << "右红外数据大小不匹配:" << rightIRData.size()
|
||
<< "(期望16bit:" << size16bit << "或8bit:" << size8bit << ")";
|
||
}
|
||
} catch (const std::exception &e) {
|
||
qDebug() << "保存右红外图像失败:" << e.what();
|
||
}
|
||
}
|
||
|
||
// 保存RGB图像(MJPEG原始数据,完整分辨率1920×1080)
|
||
if(!rgbData.isEmpty()) {
|
||
try {
|
||
// 直接保存JPEG文件(无需解码,保持原始质量)
|
||
QString rgbPath = QString("%1/%2_rgb.jpg").arg(saveDir).arg(baseName);
|
||
QFile file(rgbPath);
|
||
if(file.open(QIODevice::WriteOnly)) {
|
||
file.write(rgbData);
|
||
file.close();
|
||
qDebug() << "保存RGB图像(JPEG):" << rgbPath;
|
||
rgbSuccess = true;
|
||
} else {
|
||
qDebug() << "无法创建RGB文件:" << rgbPath;
|
||
}
|
||
} catch (const std::exception &e) {
|
||
qDebug() << "保存RGB图像失败:" << e.what();
|
||
}
|
||
}
|
||
|
||
qDebug() << "后台保存完成 - 深度图:" << (depthSuccess ? "成功" : "失败")
|
||
<< ", 点云:" << (cloudSuccess ? "成功" : "失败")
|
||
<< ", 左红外:" << (leftIRSuccess ? "成功" : "失败")
|
||
<< ", 右红外:" << (rightIRSuccess ? "成功" : "失败")
|
||
<< ", RGB:" << (rgbSuccess ? "成功" : "失败");
|
||
}
|
||
|
||
bool MainWindow::saveDepthImage(const QString &dir, const QString &baseName, const QString &format)
|
||
{
|
||
try {
|
||
// 转换QImage到OpenCV Mat
|
||
cv::Mat mat;
|
||
if(m_currentImage.format() == QImage::Format_Grayscale8) {
|
||
mat = cv::Mat(m_currentImage.height(), m_currentImage.width(), CV_8UC1,
|
||
const_cast<uchar*>(m_currentImage.bits()), m_currentImage.bytesPerLine()).clone();
|
||
} else if(m_currentImage.format() == QImage::Format_Grayscale16) {
|
||
mat = cv::Mat(m_currentImage.height(), m_currentImage.width(), CV_16UC1,
|
||
const_cast<uchar*>(m_currentImage.bits()), m_currentImage.bytesPerLine()).clone();
|
||
} else {
|
||
// 转换为灰度图
|
||
QImage grayImage = m_currentImage.convertToFormat(QImage::Format_Grayscale8);
|
||
mat = cv::Mat(grayImage.height(), grayImage.width(), CV_8UC1,
|
||
const_cast<uchar*>(grayImage.bits()), grayImage.bytesPerLine()).clone();
|
||
}
|
||
|
||
// 根据format参数保存
|
||
if(format == "png" || format == "both") {
|
||
// 保存PNG格式(8位)
|
||
QString pngPath = QString("%1/%2_depth.png").arg(dir).arg(baseName);
|
||
cv::Mat mat8bit;
|
||
if(mat.type() == CV_16UC1) {
|
||
mat.convertTo(mat8bit, CV_8UC1, 255.0 / 65535.0);
|
||
} else {
|
||
mat8bit = mat;
|
||
}
|
||
cv::imwrite(pngPath.toStdString(), mat8bit);
|
||
qDebug() << "保存PNG深度图:" << pngPath;
|
||
}
|
||
|
||
if(format == "tiff" || format == "both") {
|
||
// 保存TIFF格式(16位原始数据)
|
||
QString tiffPath = QString("%1/%2_depth.tiff").arg(dir).arg(baseName);
|
||
if(mat.type() == CV_16UC1) {
|
||
cv::imwrite(tiffPath.toStdString(), mat);
|
||
qDebug() << "保存TIFF深度图(16位):" << tiffPath;
|
||
} else {
|
||
// 如果不是16位,转换为16位保存
|
||
cv::Mat mat16bit;
|
||
mat.convertTo(mat16bit, CV_16UC1, 257.0); // 8位转16位:乘以257 (65535/255)
|
||
cv::imwrite(tiffPath.toStdString(), mat16bit);
|
||
qDebug() << "保存TIFF深度图(从8位转换):" << tiffPath;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
} catch (const std::exception &e) {
|
||
qDebug() << "保存深度图失败:" << e.what();
|
||
return false;
|
||
}
|
||
}
|
||
|
||
bool MainWindow::savePointCloud(const QString &dir, const QString &baseName, const QString &format)
|
||
{
|
||
try {
|
||
if(!m_currentPointCloud || m_currentPointCloud->empty()) {
|
||
qDebug() << "点云数据为空";
|
||
return false;
|
||
}
|
||
|
||
bool success = false;
|
||
|
||
// 根据format参数保存
|
||
if(format == "pcd" || format == "both") {
|
||
// 保存PCD格式(PCL标准格式,二进制)
|
||
QString pcdPath = QString("%1/%2_pointcloud.pcd").arg(dir).arg(baseName);
|
||
if(pcl::io::savePCDFileBinary(pcdPath.toStdString(), *m_currentPointCloud) == 0) {
|
||
qDebug() << "保存PCD点云:" << pcdPath;
|
||
success = true;
|
||
} else {
|
||
qDebug() << "保存PCD失败";
|
||
}
|
||
}
|
||
|
||
if(format == "ply" || format == "both") {
|
||
// 保存PLY格式(Polygon格式,ASCII)
|
||
QString plyPath = QString("%1/%2_pointcloud.ply").arg(dir).arg(baseName);
|
||
if(pcl::io::savePLYFileASCII(plyPath.toStdString(), *m_currentPointCloud) == 0) {
|
||
qDebug() << "保存PLY点云:" << plyPath;
|
||
success = true;
|
||
} else {
|
||
qDebug() << "保存PLY失败";
|
||
}
|
||
}
|
||
|
||
return success;
|
||
} catch (const std::exception &e) {
|
||
qDebug() << "保存点云失败:" << e.what();
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// ========== 日志功能 ==========
|
||
void MainWindow::addLog(const QString &message, const QString &level)
|
||
{
|
||
if(!m_logDisplay) return;
|
||
|
||
// 获取当前时间戳
|
||
QString timestamp = QDateTime::currentDateTime().toString("HH:mm:ss.zzz");
|
||
|
||
// 根据日志级别设置颜色
|
||
QString color;
|
||
if(level == "ERROR") {
|
||
color = "#FF6B6B"; // 红色
|
||
} else if(level == "WARNING") {
|
||
color = "#FFA500"; // 橙色
|
||
} else if(level == "SUCCESS") {
|
||
color = "#4CAF50"; // 绿色
|
||
} else {
|
||
color = "#D4D4D4"; // 默认灰白色
|
||
}
|
||
|
||
// 格式化日志消息
|
||
QString formattedMessage = QString("<span style='color: #888;'>[%1]</span> "
|
||
"<span style='color: %2;'>[%3]</span> "
|
||
"<span style='color: %4;'>%5</span>")
|
||
.arg(timestamp)
|
||
.arg(color)
|
||
.arg(level)
|
||
.arg(color)
|
||
.arg(message);
|
||
|
||
// 添加到日志显示
|
||
m_logDisplay->append(formattedMessage);
|
||
|
||
// 限制日志行数(保留最近1000行)
|
||
QTextDocument *doc = m_logDisplay->document();
|
||
if(doc->blockCount() > 1000) {
|
||
QTextCursor cursor = m_logDisplay->textCursor();
|
||
cursor.movePosition(QTextCursor::Start);
|
||
cursor.movePosition(QTextCursor::Down, QTextCursor::KeepAnchor, doc->blockCount() - 1000);
|
||
cursor.removeSelectedText();
|
||
}
|
||
|
||
// 自动滚动到底部
|
||
m_logDisplay->moveCursor(QTextCursor::End);
|
||
}
|
||
|
||
void MainWindow::onClearLogClicked()
|
||
{
|
||
if(m_logDisplay) {
|
||
m_logDisplay->clear();
|
||
addLog("日志已清除", "INFO");
|
||
}
|
||
}
|
||
|
||
void MainWindow::onSaveLogClicked()
|
||
{
|
||
QString fileName = QFileDialog::getSaveFileName(this, "保存日志",
|
||
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/viewer_log.txt",
|
||
"文本文件 (*.txt);;所有文件 (*.*)");
|
||
if(!fileName.isEmpty()) {
|
||
QFile file(fileName);
|
||
if(file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||
QTextStream out(&file);
|
||
out << m_logDisplay->toPlainText();
|
||
file.close();
|
||
addLog(QString("日志已保存到: %1").arg(fileName), "SUCCESS");
|
||
} else {
|
||
addLog(QString("保存日志失败: %1").arg(file.errorString()), "ERROR");
|
||
}
|
||
}
|
||
}
|
||
|
||
// ========== 统计信息更新 ==========
|
||
void MainWindow::updateStatistics()
|
||
{
|
||
// 更新三个相机的FPS
|
||
if(m_leftFpsLabel) {
|
||
m_leftFpsLabel->setText(QString("左红外: %1 fps").arg(m_currentLeftFps, 0, 'f', 1));
|
||
}
|
||
if(m_rightFpsLabel) {
|
||
m_rightFpsLabel->setText(QString("右红外: %1 fps").arg(m_currentRightFps, 0, 'f', 1));
|
||
}
|
||
if(m_rgbFpsLabel) {
|
||
m_rgbFpsLabel->setText(QString("RGB彩色: %1 fps").arg(m_currentRgbFps, 0, 'f', 1));
|
||
}
|
||
|
||
// 更新点云帧率
|
||
if(m_pointCloudFpsLabel) {
|
||
m_pointCloudFpsLabel->setText(QString("点云帧率: %1 fps").arg(m_currentPointCloudFps, 0, 'f', 1));
|
||
}
|
||
|
||
// 更新接收帧数(显示三个相机和点云的累计总数)
|
||
if(m_queueLabel) {
|
||
m_queueLabel->setText(QString("接收帧数: L%1 R%2 RGB%3 点云%4")
|
||
.arg(m_totalLeftFrameCount)
|
||
.arg(m_totalRightFrameCount)
|
||
.arg(m_totalRgbFrameCount)
|
||
.arg(m_totalPointCloudFrameCount));
|
||
}
|
||
}
|
||
|
||
// 监听系统主题变化事件
|
||
void MainWindow::changeEvent(QEvent *event)
|
||
{
|
||
if(event->type() == QEvent::PaletteChange) {
|
||
// 系统调色板变化,说明主题可能已切换
|
||
qDebug() << "系统主题已变化,刷新UI样式";
|
||
|
||
// 强制重新应用样式表
|
||
// 保存当前样式表
|
||
QString currentStyle = styleSheet();
|
||
|
||
// 清空样式表
|
||
setStyleSheet("");
|
||
|
||
// 重新应用样式表
|
||
setStyleSheet(currentStyle);
|
||
|
||
// 强制所有子组件更新
|
||
QList<QWidget*> widgets = findChildren<QWidget*>();
|
||
for (QWidget *widget : widgets) {
|
||
widget->style()->unpolish(widget);
|
||
widget->style()->polish(widget);
|
||
widget->update();
|
||
}
|
||
|
||
// 触发窗口重绘
|
||
update();
|
||
}
|
||
|
||
// 调用基类实现
|
||
QMainWindow::changeEvent(event);
|
||
}
|