From 93c7c1a86b1455cb108bbcdeea39ef6b615ec2b3 Mon Sep 17 00:00:00 2001 From: GloamXun Date: Wed, 21 Jan 2026 15:36:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E6=96=B0=E8=A7=84=E5=88=92ui?= =?UTF-8?q?=E5=B8=83=E5=B1=80=EF=BC=9B=E5=A2=9E=E5=8A=A0=E5=B7=A6=E5=8F=B3?= =?UTF-8?q?=E7=BA=A2=E5=A4=96=E4=BB=A5=E5=8F=8Argb=E7=9B=B8=E6=9C=BA?= =?UTF-8?q?=E5=8F=AF=E8=A7=86=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 +- include/core/GVSPParser.h | 63 +- include/gui/PointCloudGLWidget.h | 4 + src/core/GVSPParser.cpp | 306 ++++++--- src/core/NetworkManager.cpp | 72 +- src/core/NetworkManager.h | 16 +- src/core/PointCloudProcessor.cpp | 25 +- src/gui/MainWindow.cpp | 1056 ++++++++++++++++++++++++------ src/gui/MainWindow.h | 65 +- src/gui/PointCloudGLWidget.cpp | 27 +- 10 files changed, 1287 insertions(+), 352 deletions(-) diff --git a/README.md b/README.md index 2cc9e5c..79024d7 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ C:\Program Files\D330Viewer\ - ✅ UDP网络通信(GVSP协议解析) - ✅ 自动设备扫描和发现 - ✅ 相机连接管理(连接/断开) -- ✅ 命令发送(START/STOP/ONCE) +- ✅ 命令发送(START/STOP) #### 可视化 - ✅ 实时深度图显示(OpenCV,垂直翻转校正) @@ -161,7 +161,7 @@ C:\Program Files\D330Viewer\ #### 用户界面 - ✅ Qt6 GUI主窗口(三栏布局) -- ✅ 相机控制面板(START/STOP/ONCE按钮) +- ✅ 相机控制面板(START/STOP按钮) - ✅ 曝光时间调节(滑块,范围460-100000μs) - ✅ 网络配置(IP地址、端口设置) - ✅ 连接状态指示 @@ -258,7 +258,6 @@ d330viewer/ 5. **停止采集** - 点击"停止"按钮停止采集 - - 点击"单次"按钮采集单帧 ### 点云交互操作 diff --git a/include/core/GVSPParser.h b/include/core/GVSPParser.h index 192410c..88a2810 100644 --- a/include/core/GVSPParser.h +++ b/include/core/GVSPParser.h @@ -19,10 +19,16 @@ // Image format #define PIXEL_FORMAT_12BIT_GRAY 0x010C0001 +#define PIXEL_FORMAT_MONO16 0x01100005 // Mono16 format (legacy) +#define PIXEL_FORMAT_MONO16_LEFT 0x01100006 // Mono16 format for left IR camera +#define PIXEL_FORMAT_MONO16_RIGHT 0x01100007 // Mono16 format for right IR camera +#define PIXEL_FORMAT_MJPEG 0x02180001 // MJPEG format for RGB camera // Image dimensions #define IMAGE_WIDTH 1224 #define IMAGE_HEIGHT 1024 +#define RGB_WIDTH 1920 +#define RGB_HEIGHT 1080 #pragma pack(push, 1) @@ -105,39 +111,54 @@ public: void reset(); signals: - void imageReceived(const QImage &image, uint32_t blockId); - void depthDataReceived(const QByteArray &depthData, uint32_t blockId); + void leftImageReceived(const QByteArray &jpegData, uint32_t blockId); // 左红外图像(JPEG) + void rightImageReceived(const QByteArray &jpegData, uint32_t blockId); // 右红外图像(JPEG) + void rgbImageReceived(const QByteArray &jpegData, uint32_t blockId); // RGB图像(MJPEG) void pointCloudDataReceived(const QByteArray &cloudData, uint32_t blockId); void parseError(const QString &error); + // 兼容旧代码 + void imageReceived(const QImage &image, uint32_t blockId); + void depthDataReceived(const QByteArray &depthData, uint32_t blockId); + private: + // 每种数据流的独立接收状态 + struct StreamState { + bool isReceiving; + uint32_t blockId; + QByteArray dataBuffer; + size_t expectedSize; + size_t receivedSize; + int packetCount; + + // 图像特有信息 + uint32_t imageWidth; + uint32_t imageHeight; + uint32_t pixelFormat; + + StreamState() : isReceiving(false), blockId(0), expectedSize(0), + receivedSize(0), packetCount(0), imageWidth(0), + imageHeight(0), pixelFormat(0) {} + }; + void handleLeaderPacket(const uint8_t *data, size_t size); void handlePayloadPacket(const uint8_t *data, size_t size); void handleTrailerPacket(const uint8_t *data, size_t size); - void processImageData(); - void processDepthData(); - void processPointCloudData(); + void processImageData(StreamState *state); + void processDepthData(StreamState *state); + void processPointCloudData(StreamState *state); -private: - // Reception state - bool m_isReceiving; - int m_dataType; // 0=unknown, 1=image, 3=depth, 4=pointcloud - uint32_t m_currentBlockId; - - // Data buffer - QByteArray m_dataBuffer; - size_t m_expectedSize; - size_t m_receivedSize; - - // Image info - uint32_t m_imageWidth; - uint32_t m_imageHeight; - uint32_t m_pixelFormat; + // 为每种数据类型维护独立的状态 + StreamState m_leftIRState; // 左红外 + StreamState m_rightIRState; // 右红外 + StreamState m_rgbState; // RGB + StreamState m_depthState; // 深度数据 + StreamState m_pointCloudState; // 点云数据 // Statistics uint32_t m_lastBlockId; - int m_packetCount; + int m_imageSequence; // Async processing control QAtomicInt m_imageProcessingCount; diff --git a/include/gui/PointCloudGLWidget.h b/include/gui/PointCloudGLWidget.h index 3e9e116..e9fb813 100644 --- a/include/gui/PointCloudGLWidget.h +++ b/include/gui/PointCloudGLWidget.h @@ -49,6 +49,10 @@ private: std::vector m_vertices; int m_pointCount; + // 固定的点云中心点(避免抖动) + QVector3D m_fixedCenter; + bool m_centerInitialized; + // 相机参数 QMatrix4x4 m_projection; QMatrix4x4 m_view; diff --git a/src/core/GVSPParser.cpp b/src/core/GVSPParser.cpp index d49b6df..060f12d 100644 --- a/src/core/GVSPParser.cpp +++ b/src/core/GVSPParser.cpp @@ -6,16 +6,8 @@ GVSPParser::GVSPParser(QObject *parent) : QObject(parent) - , m_isReceiving(false) - , m_dataType(0) - , m_currentBlockId(0) - , m_expectedSize(0) - , m_receivedSize(0) - , m_imageWidth(0) - , m_imageHeight(0) - , m_pixelFormat(0) , m_lastBlockId(0) - , m_packetCount(0) + , m_imageSequence(0) { m_imageProcessingCount.storeRelaxed(0); } @@ -26,13 +18,12 @@ GVSPParser::~GVSPParser() void GVSPParser::reset() { - m_isReceiving = false; - m_dataType = 0; - m_currentBlockId = 0; - m_dataBuffer.clear(); - m_expectedSize = 0; - m_receivedSize = 0; - m_packetCount = 0; + m_leftIRState = StreamState(); + m_rightIRState = StreamState(); + m_rgbState = StreamState(); + m_depthState = StreamState(); + m_pointCloudState = StreamState(); + m_imageSequence = 0; } void GVSPParser::parsePacket(const QByteArray &packet) @@ -85,16 +76,12 @@ void GVSPParser::handleLeaderPacket(const uint8_t *data, size_t size) } const GVSPPacketHeader *header = reinterpret_cast(data); - m_currentBlockId = ntohs(header->block_id); + uint32_t blockId = ntohs(header->block_id); // Check payload type const uint16_t *payload_type_ptr = reinterpret_cast(data + sizeof(GVSPPacketHeader) + 2); uint16_t payload_type = ntohs(*payload_type_ptr); -// qDebug() << "[GVSPParser] Leader packet: BlockID=" << m_currentBlockId -// << "PayloadType=0x" << Qt::hex << payload_type << Qt::dec -// << "Size=" << size; - if (payload_type == PAYLOAD_TYPE_IMAGE) { // Image data leader if (size < sizeof(GVSPPacketHeader) + sizeof(GVSPImageDataLeader)) { @@ -102,64 +89,102 @@ void GVSPParser::handleLeaderPacket(const uint8_t *data, size_t size) return; } const GVSPImageDataLeader *leader = reinterpret_cast(data + sizeof(GVSPPacketHeader)); - - m_dataType = 1; - m_imageWidth = ntohl(leader->size_x); - m_imageHeight = ntohl(leader->size_y); - m_pixelFormat = ntohl(leader->pixel_format); - m_expectedSize = m_imageWidth * m_imageHeight * 2; // 12-bit packed in 16-bit - - m_dataBuffer.clear(); - m_dataBuffer.reserve(m_expectedSize); - m_receivedSize = 0; - m_isReceiving = true; - m_packetCount = 0; - // 注释掉频繁的日志输出 - // qDebug() << "Image Leader: Block" << m_currentBlockId - // << "Size:" << m_imageWidth << "x" << m_imageHeight; + uint32_t imageWidth = ntohl(leader->size_x); + uint32_t imageHeight = ntohl(leader->size_y); + uint32_t pixelFormat = ntohl(leader->pixel_format); + + // 根据像素格式选择对应的状态 + StreamState *state = nullptr; + if (pixelFormat == PIXEL_FORMAT_MONO16_LEFT) { + state = &m_leftIRState; + } else if (pixelFormat == PIXEL_FORMAT_MONO16_RIGHT) { + state = &m_rightIRState; + } else if (pixelFormat == PIXEL_FORMAT_MJPEG) { + state = &m_rgbState; + } else if (pixelFormat == PIXEL_FORMAT_MONO16 || pixelFormat == PIXEL_FORMAT_12BIT_GRAY) { + // Legacy格式:根据序号分配 + int imageType = m_imageSequence % 2; + state = (imageType == 0) ? &m_leftIRState : &m_rightIRState; + } + + if (state) { + state->blockId = blockId; + state->imageWidth = imageWidth; + state->imageHeight = imageHeight; + state->pixelFormat = pixelFormat; + + // 根据pixel format设置期望大小 + if (pixelFormat == PIXEL_FORMAT_MJPEG) { + // MJPEG是压缩格式,实际大小未知,设置为0表示动态接收 + state->expectedSize = 0; + } else { + // 16-bit或12-bit灰度等固定格式 + state->expectedSize = imageWidth * imageHeight * 2; + } + + state->dataBuffer.clear(); + if (state->expectedSize > 0) { + state->dataBuffer.reserve(state->expectedSize); + } + state->receivedSize = 0; + state->isReceiving = true; + state->packetCount = 0; + } } else if (payload_type == PAYLOAD_TYPE_BINARY) { // Depth data leader const GVSPBinaryDataLeader *leader = reinterpret_cast(data + sizeof(GVSPPacketHeader)); - m_dataType = 3; - m_expectedSize = ntohl(leader->file_size); - - m_dataBuffer.clear(); - m_dataBuffer.reserve(m_expectedSize); - m_receivedSize = 0; - m_isReceiving = true; - m_packetCount = 0; - - // 注释掉频繁的日志输出 - // qDebug() << "Depth Leader: Block" << m_currentBlockId - // << "Size:" << m_expectedSize << "bytes"; + m_depthState.blockId = blockId; + m_depthState.expectedSize = ntohl(leader->file_size); + m_depthState.dataBuffer.clear(); + m_depthState.dataBuffer.reserve(m_depthState.expectedSize); + m_depthState.receivedSize = 0; + m_depthState.isReceiving = true; + m_depthState.packetCount = 0; } else if (payload_type == PAYLOAD_TYPE_POINTCLOUD) { // Point cloud data leader (vendor-specific 0x8000) const GVSPPointCloudDataLeader *leader = reinterpret_cast(data + sizeof(GVSPPacketHeader)); - m_dataType = 4; // 新类型:点云数据 - m_expectedSize = ntohl(leader->data_size); - - m_dataBuffer.clear(); - m_dataBuffer.reserve(m_expectedSize); - m_receivedSize = 0; - m_isReceiving = true; - m_packetCount = 0; - - // qDebug() << "[PointCloud Leader] Block:" << m_currentBlockId - // << "Expected Size:" << m_expectedSize << "bytes"; + m_pointCloudState.blockId = blockId; + m_pointCloudState.expectedSize = ntohl(leader->data_size); + m_pointCloudState.dataBuffer.clear(); + m_pointCloudState.dataBuffer.reserve(m_pointCloudState.expectedSize); + m_pointCloudState.receivedSize = 0; + m_pointCloudState.isReceiving = true; + m_pointCloudState.packetCount = 0; } } void GVSPParser::handlePayloadPacket(const uint8_t *data, size_t size) { - if (!m_isReceiving) { + if (size < sizeof(GVSPPacketHeader)) { return; } + const GVSPPacketHeader *header = reinterpret_cast(data); + uint32_t blockId = ntohs(header->block_id); + + // 查找匹配的状态 + StreamState *state = nullptr; + if (m_leftIRState.isReceiving && m_leftIRState.blockId == blockId) { + state = &m_leftIRState; + } else if (m_rightIRState.isReceiving && m_rightIRState.blockId == blockId) { + state = &m_rightIRState; + } else if (m_rgbState.isReceiving && m_rgbState.blockId == blockId) { + state = &m_rgbState; + } else if (m_depthState.isReceiving && m_depthState.blockId == blockId) { + state = &m_depthState; + } else if (m_pointCloudState.isReceiving && m_pointCloudState.blockId == blockId) { + state = &m_pointCloudState; + } + + if (!state) { + return; // 没有匹配的接收状态 + } + if (size <= sizeof(GVSPPacketHeader)) { return; } @@ -169,58 +194,130 @@ void GVSPParser::handlePayloadPacket(const uint8_t *data, size_t size) size_t payload_size = size - sizeof(GVSPPacketHeader); // Append to buffer - m_dataBuffer.append(reinterpret_cast(payload), payload_size); - m_receivedSize += payload_size; - m_packetCount++; + state->dataBuffer.append(reinterpret_cast(payload), payload_size); + state->receivedSize += payload_size; + state->packetCount++; } void GVSPParser::handleTrailerPacket(const uint8_t *data, size_t size) { - if (!m_isReceiving) { + if (size < sizeof(GVSPPacketHeader)) { return; } - // 注释掉频繁的日志输出 - // qDebug() << "Trailer received: Block" << m_currentBlockId - // << "Received" << m_receivedSize << "/" << m_expectedSize << "bytes" - // << "Packets:" << m_packetCount; + const GVSPPacketHeader *header = reinterpret_cast(data); + uint32_t blockId = ntohs(header->block_id); - // Process complete data - if (m_dataType == 1) { - processImageData(); - } else if (m_dataType == 3) { - processDepthData(); - } else if (m_dataType == 4) { - processPointCloudData(); + // 查找匹配的状态并处理 + if (m_leftIRState.isReceiving && m_leftIRState.blockId == blockId) { + processImageData(&m_leftIRState); + m_leftIRState.isReceiving = false; + m_lastBlockId = blockId; + } else if (m_rightIRState.isReceiving && m_rightIRState.blockId == blockId) { + processImageData(&m_rightIRState); + m_rightIRState.isReceiving = false; + m_lastBlockId = blockId; + } else if (m_rgbState.isReceiving && m_rgbState.blockId == blockId) { + processImageData(&m_rgbState); + m_rgbState.isReceiving = false; + m_lastBlockId = blockId; + } else if (m_depthState.isReceiving && m_depthState.blockId == blockId) { + processDepthData(&m_depthState); + m_depthState.isReceiving = false; + m_lastBlockId = blockId; + } else if (m_pointCloudState.isReceiving && m_pointCloudState.blockId == blockId) { + processPointCloudData(&m_pointCloudState); + m_pointCloudState.isReceiving = false; + m_lastBlockId = blockId; } - - // Reset state - m_isReceiving = false; - m_lastBlockId = m_currentBlockId; } -void GVSPParser::processImageData() +void GVSPParser::processImageData(GVSPParser::StreamState *state) { - if (m_dataBuffer.size() < m_expectedSize) { + if (!state) return; + + // 处理MJPEG格式(RGB相机) + if (state->pixelFormat == PIXEL_FORMAT_MJPEG) { + // RGB MJPEG + emit rgbImageReceived(state->dataBuffer, state->blockId); + m_imageSequence++; + return; + } + + // 处理Mono16格式(左右红外相机原始16位数据) + // 使用像素格式直接区分左右,不依赖接收顺序 + if (state->pixelFormat == PIXEL_FORMAT_MONO16_LEFT) { + // 检查数据大小 + if (state->dataBuffer.size() < state->expectedSize) { + return; + } + // 左红外原始数据 + emit leftImageReceived(state->dataBuffer, state->blockId); + m_imageSequence++; + return; + } + + if (state->pixelFormat == PIXEL_FORMAT_MONO16_RIGHT) { + // 检查数据大小 + if (state->dataBuffer.size() < state->expectedSize) { + return; + } + // 右红外原始数据 + emit rightImageReceived(state->dataBuffer, state->blockId); + m_imageSequence++; + return; + } + + // 兼容旧版本:使用序号区分(legacy) + if (state->pixelFormat == PIXEL_FORMAT_MONO16) { + // 检查数据大小 + if (state->dataBuffer.size() < state->expectedSize) { + return; + } + + // 根据图像序号区分:偶数=左红外,奇数=右红外 + int imageType = m_imageSequence % 2; + + if (imageType == 0) { + // 左红外原始数据 + emit leftImageReceived(state->dataBuffer, state->blockId); + } else { + // 右红外原始数据 + emit rightImageReceived(state->dataBuffer, state->blockId); + } + + m_imageSequence++; + return; + } + + // 处理12-bit灰度格式 - 固定大小,需要检查 + if (state->dataBuffer.size() < state->expectedSize) { + return; + } + + // 处理12-bit灰度图像(左右红外相机) + if (state->pixelFormat != PIXEL_FORMAT_12BIT_GRAY) { return; } // 节流机制:如果已有3个或更多图像在处理中,跳过当前帧 if (m_imageProcessingCount.loadAcquire() >= 3) { + m_imageSequence++; return; } // 增加处理计数 m_imageProcessingCount.ref(); - // 复制数据到局部变量,避免在异步处理时数据被覆盖 - QByteArray dataCopy = m_dataBuffer; - uint32_t blockId = m_currentBlockId; - size_t width = m_imageWidth; - size_t height = m_imageHeight; + // 复制数据到局部变量 + QByteArray dataCopy = state->dataBuffer; + uint32_t blockId = state->blockId; + size_t width = state->imageWidth; + size_t height = state->imageHeight; + int imageSeq = m_imageSequence; // 使用QtConcurrent在后台线程处理图像数据 - QtConcurrent::run([this, dataCopy, blockId, width, height]() { + QtConcurrent::run([this, dataCopy, blockId, width, height, imageSeq]() { // Convert 16-bit depth data to 8-bit grayscale for display const uint16_t *src = reinterpret_cast(dataCopy.constData()); QImage image(width, height, QImage::Format_Grayscale8); @@ -238,48 +335,49 @@ void GVSPParser::processImageData() } } - // 归一化并水平翻转(修正左右镜像) + // 归一化(不翻转,保持原始方向) uint8_t *dst = image.bits(); float scale = (maxVal > minVal) ? (255.0f / (maxVal - minVal)) : 0.0f; for (size_t row = 0; row < height; row++) { for (size_t col = 0; col < width; col++) { - size_t srcIdx = row * width + col; - size_t dstIdx = row * width + (width - 1 - col); // 水平翻转 - uint16_t val = src[srcIdx]; - dst[dstIdx] = (val == 0) ? 0 : static_cast((val - minVal) * scale); + size_t idx = row * width + col; + uint16_t val = src[idx]; + dst[idx] = (val == 0) ? 0 : static_cast((val - minVal) * scale); } } + // 注意:此代码路径已废弃,下位机现在发送JPEG格式 + // 仅保留兼容旧代码的信号 emit imageReceived(image, blockId); // 减少处理计数 m_imageProcessingCount.deref(); }); + + // 增加图像序号 + m_imageSequence++; } -void GVSPParser::processDepthData() +void GVSPParser::processDepthData(GVSPParser::StreamState *state) { - if (m_dataBuffer.size() < m_expectedSize) { - // 注释掉频繁的警告日志 - // qDebug() << "Warning: Incomplete depth data" << m_dataBuffer.size() << "/" << m_expectedSize; + if (!state) return; + + if (state->dataBuffer.size() < state->expectedSize) { return; } - emit depthDataReceived(m_dataBuffer, m_currentBlockId); + emit depthDataReceived(state->dataBuffer, state->blockId); } -void GVSPParser::processPointCloudData() +void GVSPParser::processPointCloudData(GVSPParser::StreamState *state) { - // qDebug() << "[PointCloud] Received:" << m_dataBuffer.size() - // << "bytes, Expected:" << m_expectedSize << "bytes"; + if (!state) return; - if (m_dataBuffer.size() < m_expectedSize) { - qDebug() << "[PointCloud] ERROR: Data incomplete, skipping!"; + if (state->dataBuffer.size() < state->expectedSize) { return; } - // qDebug() << "[PointCloud] Data complete, emitting pointCloudDataReceived signal..."; // 点云数据直接发送,格式为 short 数组 (x, y, z, x, y, z, ...) - emit pointCloudDataReceived(m_dataBuffer, m_currentBlockId); + emit pointCloudDataReceived(state->dataBuffer, state->blockId); } diff --git a/src/core/NetworkManager.cpp b/src/core/NetworkManager.cpp index fc53213..3b8ee3e 100644 --- a/src/core/NetworkManager.cpp +++ b/src/core/NetworkManager.cpp @@ -18,6 +18,9 @@ NetworkManager::NetworkManager(QObject *parent) // 连接GVSP解析器信号 connect(m_gvspParser, &GVSPParser::imageReceived, this, &NetworkManager::imageReceived); + connect(m_gvspParser, &GVSPParser::leftImageReceived, this, &NetworkManager::leftImageReceived); + connect(m_gvspParser, &GVSPParser::rightImageReceived, this, &NetworkManager::rightImageReceived); + connect(m_gvspParser, &GVSPParser::rgbImageReceived, this, &NetworkManager::rgbImageReceived); connect(m_gvspParser, &GVSPParser::depthDataReceived, this, &NetworkManager::depthDataReceived); connect(m_gvspParser, &GVSPParser::pointCloudDataReceived, this, &NetworkManager::pointCloudDataReceived); } @@ -54,8 +57,8 @@ bool NetworkManager::connectToCamera(const QString &ip, int controlPort, int dat return false; } - // 设置UDP接收缓冲区大小为64MB(减少丢包) - m_dataSocket->setSocketOption(QAbstractSocket::ReceiveBufferSizeSocketOption, QVariant(64 * 1024 * 1024)); + // 设置UDP接收缓冲区大小为256MB(最大化,减少丢包) + m_dataSocket->setSocketOption(QAbstractSocket::ReceiveBufferSizeSocketOption, QVariant(256 * 1024 * 1024)); qDebug() << "Successfully bound data port" << m_dataPort; qDebug() << "Data socket state:" << m_dataSocket->state(); @@ -129,15 +132,66 @@ bool NetworkManager::sendStopCommand() return sendCommand("STOP"); } -bool NetworkManager::sendOnceCommand() -{ - return sendCommand("ONCE"); -} - bool NetworkManager::sendExposureCommand(int exposureTime) { - QString command = QString("EXPOSURE:%1").arg(exposureTime); - return sendCommand(command); + // 同时发送结构光曝光命令(UART控制激光器,单位μs) + QString exposureCommand = QString("EXPOSURE:%1").arg(exposureTime); + bool success1 = sendCommand(exposureCommand); + + // 同时发送红外相机曝光命令(通过触发脉冲宽度控制,单位μs) + // 下位机会将此值用作manual_trigger_pulse()的脉冲宽度参数 + // 脉冲宽度直接决定相机的实际曝光时间 + int irExposure = exposureTime; + + // 限制在有效范围内(1000μs ~ 100000μs,避免脉冲太短导致相机无法触发) + if (irExposure < 1000) irExposure = 1000; + if (irExposure > 100000) irExposure = 100000; + + QString irExposureCommand = QString("IR_EXPOSURE:%1").arg(irExposure); + bool success2 = sendCommand(irExposureCommand); + + return success1 && success2; +} + +// ========== 传输开关命令 ========== +bool NetworkManager::sendEnableLeftIR() +{ + return sendCommand("ENABLE_LEFT"); +} + +bool NetworkManager::sendDisableLeftIR() +{ + return sendCommand("DISABLE_LEFT"); +} + +bool NetworkManager::sendEnableRightIR() +{ + return sendCommand("ENABLE_RIGHT"); +} + +bool NetworkManager::sendDisableRightIR() +{ + return sendCommand("DISABLE_RIGHT"); +} + +bool NetworkManager::sendEnableRGB() +{ + return sendCommand("ENABLE_RGB"); +} + +bool NetworkManager::sendDisableRGB() +{ + return sendCommand("DISABLE_RGB"); +} + +bool NetworkManager::sendMonocularMode() +{ + return sendCommand("MONOCULAR"); +} + +bool NetworkManager::sendBinocularMode() +{ + return sendCommand("BINOCULAR"); } // ========== 槽函数 ========== diff --git a/src/core/NetworkManager.h b/src/core/NetworkManager.h index cd33e8f..aa9ff91 100644 --- a/src/core/NetworkManager.h +++ b/src/core/NetworkManager.h @@ -26,15 +26,29 @@ public: bool sendCommand(const QString &command); bool sendStartCommand(); bool sendStopCommand(); - bool sendOnceCommand(); bool sendExposureCommand(int exposureTime); + // 发送传输开关命令 + bool sendEnableLeftIR(); + bool sendDisableLeftIR(); + bool sendEnableRightIR(); + bool sendDisableRightIR(); + bool sendEnableRGB(); + bool sendDisableRGB(); + + // 发送单目/双目模式切换命令 + bool sendMonocularMode(); + bool sendBinocularMode(); + signals: void connected(); void disconnected(); void errorOccurred(const QString &error); void dataReceived(const QByteArray &data); void imageReceived(const QImage &image, uint32_t blockId); + void leftImageReceived(const QByteArray &jpegData, uint32_t blockId); + void rightImageReceived(const QByteArray &jpegData, uint32_t blockId); + void rgbImageReceived(const QByteArray &jpegData, uint32_t blockId); void depthDataReceived(const QByteArray &depthData, uint32_t blockId); void pointCloudDataReceived(const QByteArray &cloudData, uint32_t blockId); diff --git a/src/core/PointCloudProcessor.cpp b/src/core/PointCloudProcessor.cpp index a5e371c..1cfab1f 100644 --- a/src/core/PointCloudProcessor.cpp +++ b/src/core/PointCloudProcessor.cpp @@ -284,30 +284,29 @@ void PointCloudProcessor::processPointCloudData(const QByteArray &cloudData, uin const int16_t* cloudShort = reinterpret_cast(cloudData.constData()); if (isZOnly) { - // Z-only格式:直接映射为平面点云(类似图像平面) - // X对应列(左右翻转),Y对应行(上下翻转),Z是深度值 - // qDebug() << "[PointCloud] Processing Z-only format as planar mapping"; - + // Z-only格式:转换为正交投影(柱形) for (size_t i = 0; i < m_totalPoints; i++) { int row = i / m_imageWidth; int col = i % m_imageWidth; - // 直接映射:X=列(翻转),Y=行(翻转),Z=深度 - float x = static_cast(m_imageWidth - 1 - col); // 左右翻转 - float y = static_cast(m_imageHeight - 1 - row); // 上下翻转 + // 读取深度值(单位:毫米) float z = static_cast(cloudShort[i]) * m_zScale; - cloud->points[i].x = x; - cloud->points[i].y = y; + // 正交投影:X、Y使用像素坐标(Y轴翻转以修正镜像) + cloud->points[i].x = static_cast(col); + cloud->points[i].y = static_cast(m_imageHeight - 1 - row); cloud->points[i].z = z; } } else { // XYZ格式:完整的三维坐标 - // qDebug() << "[PointCloud] Processing XYZ format"; - + // 转换为正交投影(柱形),使用像素坐标作为X、Y for (size_t i = 0; i < m_totalPoints; i++) { - cloud->points[i].x = static_cast(cloudShort[i * 3 + 0]) * m_zScale; - cloud->points[i].y = static_cast(cloudShort[i * 3 + 1]) * m_zScale; + int row = i / m_imageWidth; + int col = i % m_imageWidth; + + // 正交投影:X、Y使用像素坐标(Y轴翻转以修正镜像),Z使用深度值 + cloud->points[i].x = static_cast(col); + cloud->points[i].y = static_cast(m_imageHeight - 1 - row); cloud->points[i].z = static_cast(cloudShort[i * 3 + 2]) * m_zScale; } } diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index e867ceb..0f64a9e 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -46,7 +46,6 @@ MainWindow::MainWindow(QWidget *parent) , m_pointCloudProcessor(std::make_unique(this)) , m_updateTimer(new QTimer(this)) , m_isConnected(false) - , m_autoSaveOnNextFrame(false) , m_currentPointCloud(new pcl::PointCloud()) , m_currentFrameId(0) , m_depthFrameCount(0) @@ -55,7 +54,21 @@ MainWindow::MainWindow(QWidget *parent) , 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_leftIREnabled.storeRelaxed(1); // 初始化左红外启用标志(默认启用) + m_rightIREnabled.storeRelaxed(1); // 初始化右红外启用标志(默认启用) + m_rgbEnabled.storeRelaxed(1); // 初始化RGB启用标志(默认启用) setupUI(); setupConnections(); loadSettings(); @@ -94,184 +107,164 @@ void MainWindow::setupUI() { setWindowTitle("D330Viewer - D330M 相机控制"); resize(1280, 720); // 16:9 比例 - // resize(720, 1280); // 16:9 比例 // 设置应用程序图标 setWindowIcon(QIcon(":/icons/icons/app_icon.png")); - // 移除全局样式表以支持系统深色模式 - // Qt会自动使用系统主题颜色 - // 创建中央控件 m_centralWidget = new QWidget(this); setCentralWidget(m_centralWidget); - // 创建左侧控制面板 - m_controlPanel = new QWidget(m_centralWidget); - m_controlPanel->setMinimumWidth(320); - m_controlPanel->setMaximumWidth(420); - // 移除固定背景色,使用系统默认颜色以支持深色模式 + // 创建主布局 + QVBoxLayout *mainLayout = new QVBoxLayout(m_centralWidget); + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(0); - // 创建右侧内容区域(包含显示区和日志区) - QWidget *rightPanel = new QWidget(m_centralWidget); - QVBoxLayout *rightLayout = new QVBoxLayout(rightPanel); - rightLayout->setContentsMargins(0, 0, 0, 0); - rightLayout->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); - // 创建上方显示区域的水平分割器(图像 + 点云) - QSplitter *displaySplitter = new QSplitter(Qt::Horizontal, rightPanel); + // 相机控制按钮(纯图标) + 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); - // 创建图像显示区域 - m_imageDisplay = new QLabel(displaySplitter); - m_imageDisplay->setMinimumSize(480, 360); // 4:3 比例,减小高度 - m_imageDisplay->setStyleSheet( - "QLabel { " - " background-color: #263238; " - " color: #B0BEC5; " - " border: 2px solid #37474F; " - " border-radius: 4px; " - " font-size: 12pt; " - "}" - ); - m_imageDisplay->setAlignment(Qt::AlignCenter); - m_imageDisplay->setText("图像显示\n(等待数据...)"); + // 设置按钮大小和图标大小 + QSize btnSize(40, 40); + QSize iconSize(24, 24); - // 创建点云显示区域 - m_pointCloudWidget = new PointCloudWidget(displaySplitter); - m_pointCloudWidget->setMinimumSize(480, 360); // 4:3 比例,减小高度 + m_startBtn->setFixedSize(btnSize); + m_stopBtn->setFixedSize(btnSize); + m_captureBtn->setFixedSize(btnSize); - // 设置显示区域分割器比例 - displaySplitter->setStretchFactor(0, 1); - displaySplitter->setStretchFactor(1, 1); + m_startBtn->setIconSize(iconSize); + m_stopBtn->setIconSize(iconSize); + m_captureBtn->setIconSize(iconSize); - // 创建日志显示区域 - QWidget *logWidget = new QWidget(rightPanel); - QVBoxLayout *logLayout = new QVBoxLayout(logWidget); - logLayout->setContentsMargins(5, 5, 5, 5); - logLayout->setSpacing(5); + // 设置工具提示 + m_startBtn->setToolTip("启动连续采集"); + m_stopBtn->setToolTip("停止采集"); + m_captureBtn->setToolTip("拍照保存"); - // 日志标题和按钮 - 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 { color: #424242; 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; }" + // 按钮样式 + 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; }" + ); - logHeaderLayout->addWidget(logTitle); - logHeaderLayout->addStretch(); - logHeaderLayout->addWidget(m_clearLogBtn); - logHeaderLayout->addWidget(m_saveLogBtn); - logLayout->addLayout(logHeaderLayout); + toolBarLayout->addWidget(m_startBtn); + toolBarLayout->addWidget(m_stopBtn); + toolBarLayout->addWidget(m_captureBtn); + toolBarLayout->addSpacing(15); - // 日志文本显示 - 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); + // 分隔线 + QFrame *separator1 = new QFrame(topToolBar); + separator1->setFrameShape(QFrame::VLine); + separator1->setFrameShadow(QFrame::Sunken); + toolBarLayout->addWidget(separator1); + toolBarLayout->addSpacing(10); - // 将显示区和日志区添加到右侧面板的垂直分割器中 - QSplitter *rightVerticalSplitter = new QSplitter(Qt::Vertical, rightPanel); - rightVerticalSplitter->addWidget(displaySplitter); - rightVerticalSplitter->addWidget(logWidget); - rightVerticalSplitter->setStretchFactor(0, 3); // 显示区占更多空间 - rightVerticalSplitter->setStretchFactor(1, 1); // 日志区占较少空间 + // 数据传输开关按钮 + m_leftIRToggle = new QPushButton("左红外", topToolBar); + m_rightIRToggle = new QPushButton("右红外", topToolBar); + m_rgbToggle = new QPushButton("RGB", topToolBar); - rightLayout->addWidget(rightVerticalSplitter); + m_leftIRToggle->setCheckable(true); + m_rightIRToggle->setCheckable(true); + m_rgbToggle->setCheckable(true); - // 创建主水平分割器(左侧控制面板 + 右侧内容区) - m_mainSplitter = new QSplitter(Qt::Horizontal, m_centralWidget); - m_mainSplitter->addWidget(m_controlPanel); - m_mainSplitter->addWidget(rightPanel); - m_mainSplitter->setStretchFactor(0, 0); // 控制面板固定宽度 - m_mainSplitter->setStretchFactor(1, 1); // 右侧内容区自适应 + m_leftIRToggle->setChecked(true); + m_rightIRToggle->setChecked(true); + m_rgbToggle->setChecked(true); - // 主布局 - QVBoxLayout *mainLayout = new QVBoxLayout(m_centralWidget); - mainLayout->addWidget(m_mainSplitter); - mainLayout->setContentsMargins(0, 0, 0, 0); + 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(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); - // ========== 相机控制组 ========== - QGroupBox *cameraGroup = new QGroupBox("相机控制", m_controlPanel); - QVBoxLayout *cameraLayout = new QVBoxLayout(cameraGroup); - - // START/STOP/ONCE按钮 - QHBoxLayout *buttonLayout = new QHBoxLayout(); - m_startBtn = new QPushButton(QIcon(":/icons/icons/start.png"), "启动", cameraGroup); - m_stopBtn = new QPushButton(QIcon(":/icons/icons/stop.png"), "停止", cameraGroup); - m_onceBtn = new QPushButton(QIcon(":/icons/icons/camera.png"), "单次", cameraGroup); - - m_startBtn->setMinimumHeight(45); - m_stopBtn->setMinimumHeight(45); - m_onceBtn->setMinimumHeight(45); - m_startBtn->setIconSize(QSize(22, 22)); - m_stopBtn->setIconSize(QSize(22, 22)); - m_onceBtn->setIconSize(QSize(22, 22)); - - // 为启动按钮设置绿色主题 - m_startBtn->setStyleSheet( - "QPushButton { background-color: #4CAF50; color: white; }" - "QPushButton:hover { background-color: #45A049; }" - "QPushButton:pressed { background-color: #388E3C; }" - ); - - // 为停止按钮设置红色主题 - m_stopBtn->setStyleSheet( - "QPushButton { background-color: #F44336; color: white; }" - "QPushButton:hover { background-color: #E53935; }" - "QPushButton:pressed { background-color: #C62828; }" - ); - - // 为单次按钮设置蓝色主题 - m_onceBtn->setStyleSheet( - "QPushButton { background-color: #2196F3; color: white; }" - "QPushButton:hover { background-color: #1E88E5; }" - "QPushButton:pressed { background-color: #1976D2; }" - ); - - buttonLayout->addWidget(m_startBtn); - buttonLayout->addWidget(m_stopBtn); - buttonLayout->addWidget(m_onceBtn); - cameraLayout->addLayout(buttonLayout); - - // 拍照按钮 - m_captureBtn = new QPushButton(QIcon(":/icons/icons/camera.png"), "拍照", cameraGroup); - m_captureBtn->setMinimumHeight(45); - m_captureBtn->setIconSize(QSize(22, 22)); - m_captureBtn->setStyleSheet( - "QPushButton { background-color: #FF9800; color: white; }" - "QPushButton:hover { background-color: #FB8C00; }" - "QPushButton:pressed { background-color: #EF6C00; }" - ); - cameraLayout->addWidget(m_captureBtn); - - controlLayout->addWidget(cameraGroup); - // ========== 选项卡组件 ========== QTabWidget *tabWidget = new QTabWidget(m_controlPanel); tabWidget->setStyleSheet("QTabWidget::pane { border: 1px solid #CCCCCC; }"); @@ -370,11 +363,11 @@ void MainWindow::setupUI() QHBoxLayout *exposureLayout = new QHBoxLayout(); m_exposureSlider = new QSlider(Qt::Horizontal, exposureGroup); - m_exposureSlider->setRange(460, 100000); + m_exposureSlider->setRange(1000, 100000); m_exposureSlider->setValue(10000); m_exposureSpinBox = new QSpinBox(exposureGroup); - m_exposureSpinBox->setRange(460, 100000); + m_exposureSpinBox->setRange(1000, 100000); m_exposureSpinBox->setValue(10000); m_exposureSpinBox->setMinimumWidth(80); @@ -440,25 +433,174 @@ void MainWindow::setupUI() QGroupBox *statsGroup = new QGroupBox("统计信息", m_controlPanel); QVBoxLayout *statsLayout = new QVBoxLayout(statsGroup); - m_depthFpsLabel = new QLabel("红外图帧率: 0.0 fps", statsGroup); - m_pointCloudFpsLabel = new QLabel("点云帧率: 0.0 fps", statsGroup); - m_resolutionLabel = new QLabel("分辨率: 1224 x 1024", statsGroup); - m_queueLabel = new QLabel("接收帧数: 0", 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); - // 设置统计标签样式 - 移除固定颜色以支持深色模式 - QString statsLabelStyle = "QLabel { font-size: 10pt; padding: 4px; }"; - m_depthFpsLabel->setStyleSheet(statsLabelStyle); - m_pointCloudFpsLabel->setStyleSheet(statsLabelStyle); - m_resolutionLabel->setStyleSheet(statsLabelStyle); - m_queueLabel->setStyleSheet(statsLabelStyle); + // 兼容旧代码 + m_depthFpsLabel = m_leftFpsLabel; - statsLayout->addWidget(m_depthFpsLabel); + 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); - controlLayout->addStretch(); + + // ========== 创建右侧面板 ========== + 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() @@ -466,7 +608,6 @@ void MainWindow::setupConnections() // 相机控制按钮连接 connect(m_startBtn, &QPushButton::clicked, this, &MainWindow::onStartClicked); connect(m_stopBtn, &QPushButton::clicked, this, &MainWindow::onStopClicked); - connect(m_onceBtn, &QPushButton::clicked, this, &MainWindow::onOnceClicked); connect(m_captureBtn, &QPushButton::clicked, this, &MainWindow::onCaptureClicked); connect(m_browseSavePathBtn, &QPushButton::clicked, this, &MainWindow::onBrowseSavePathClicked); @@ -487,6 +628,9 @@ void MainWindow::setupConnections() // 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); @@ -507,6 +651,73 @@ void MainWindow::setupConnections() // 日志按钮连接 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_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() @@ -565,14 +776,6 @@ void MainWindow::onStopClicked() addLog("发送停止命令", "INFO"); } -void MainWindow::onOnceClicked() -{ - qDebug() << "单次按钮点击 - 将在接收到数据后自动保存"; - m_autoSaveOnNextFrame = true; - m_networkManager->sendOnceCommand(); - addLog("发送单次采集命令(将自动保存)", "INFO"); -} - void MainWindow::onExposureChanged(int value) { qDebug() << "曝光值改变为:" << value; @@ -638,6 +841,27 @@ void MainWindow::onNetworkConnected() 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() @@ -752,6 +976,345 @@ void MainWindow::onImageReceived(const QImage &image, uint32_t blockId) } } +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; + } + + // 使用后台线程处理16位原始数据,避免阻塞UI + if (m_leftImageDisplay && jpegData.size() > 0) { + // 检查数据大小是否为16位图像 + size_t expectedSize = 1224 * 1024 * sizeof(uint16_t); + if (jpegData.size() == expectedSize) { + // 复制数据到局部变量 + QByteArray dataCopy = jpegData; + + // 在后台线程处理 + QtConcurrent::run([this, dataCopy]() { + try { + const uint16_t* src = reinterpret_cast(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((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(); + } + } +} + +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; + } + + // 使用后台线程处理16位原始数据,避免阻塞UI + if (m_rightImageDisplay && jpegData.size() > 0) { + // 检查数据大小是否为16位图像 + size_t expectedSize = 1224 * 1024 * sizeof(uint16_t); + if (jpegData.size() == expectedSize) { + // 复制数据到局部变量 + QByteArray dataCopy = jpegData; + + // 在后台线程处理 + QtConcurrent::run([this, dataCopy]() { + try { + const uint16_t* src = reinterpret_cast(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((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(); + } + } +} + +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 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) { // 实时处理每一帧 @@ -800,17 +1363,6 @@ void MainWindow::onPointCloudReady(pcl::PointCloud::Ptr cloud, ui if (m_pointCloudWidget) { m_pointCloudWidget->updatePointCloud(cloud); } - - // 如果是单次拍照模式,自动保存 - if (m_autoSaveOnNextFrame) { - m_autoSaveOnNextFrame = false; - qDebug() << "单次拍照完成,自动保存数据..."; - - // 触发拍照保存 - QTimer::singleShot(100, this, [this]() { - onCaptureClicked(); - }); - } } // ========== 拍照功能 ========== @@ -855,12 +1407,19 @@ void MainWindow::onCaptureClicked() QImage imageCopy = m_currentImage.copy(); pcl::PointCloud::Ptr cloudCopy(new pcl::PointCloud(*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]() { - this->performBackgroundSave(saveDir, baseName, depthFormat, pointCloudFormat, imageCopy, cloudCopy); + QtConcurrent::run([this, saveDir, baseName, depthFormat, pointCloudFormat, + imageCopy, cloudCopy, leftIRCopy, rightIRCopy, rgbCopy]() { + this->performBackgroundSave(saveDir, baseName, depthFormat, pointCloudFormat, + imageCopy, cloudCopy, leftIRCopy, rightIRCopy, rgbCopy); }); } @@ -879,7 +1438,9 @@ void MainWindow::onBrowseSavePathClicked() void MainWindow::performBackgroundSave(const QString &saveDir, const QString &baseName, const QString &depthFormat, const QString &pointCloudFormat, - const QImage &image, pcl::PointCloud::Ptr cloud) + const QImage &image, pcl::PointCloud::Ptr cloud, + const QByteArray &leftIRData, const QByteArray &rightIRData, + const QByteArray &rgbData) { qDebug() << "后台保存线程开始..."; @@ -963,8 +1524,109 @@ void MainWindow::performBackgroundSave(const QString &saveDir, const QString &ba } } + // 保存三个相机的原始图像(完整分辨率) + bool leftIRSuccess = false; + bool rightIRSuccess = false; + bool rgbSuccess = false; + + // 保存左红外图像(16位原始数据,1224×1024) + if (!leftIRData.isEmpty()) { + try { + size_t expectedSize = 1224 * 1024 * sizeof(uint16_t); + if (leftIRData.size() == expectedSize) { + const uint16_t* src = reinterpret_cast(leftIRData.constData()); + + // 创建16位灰度图像 + cv::Mat leftIR16(1024, 1224, CV_16UC1); + memcpy(leftIR16.data, src, expectedSize); + + // 根据depthFormat参数保存 + if (depthFormat == "png" || depthFormat == "both") { + // 保存PNG格式(8位) + 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图像:" << pngPath; + leftIRSuccess = true; + } + + if (depthFormat == "tiff" || depthFormat == "both") { + // 保存TIFF格式(保留16位精度) + QString tiffPath = QString("%1/%2_left_ir.tiff").arg(saveDir).arg(baseName); + cv::imwrite(tiffPath.toStdString(), leftIR16); + qDebug() << "保存左红外TIFF图像(16位):" << tiffPath; + leftIRSuccess = true; + } + } else { + qDebug() << "左红外数据大小不匹配:" << leftIRData.size(); + } + } catch (const std::exception &e) { + qDebug() << "保存左红外图像失败:" << e.what(); + } + } + + // 保存右红外图像(16位原始数据,1224×1024) + if (!rightIRData.isEmpty()) { + try { + size_t expectedSize = 1224 * 1024 * sizeof(uint16_t); + if (rightIRData.size() == expectedSize) { + const uint16_t* src = reinterpret_cast(rightIRData.constData()); + + // 创建16位灰度图像 + cv::Mat rightIR16(1024, 1224, CV_16UC1); + memcpy(rightIR16.data, src, expectedSize); + + // 根据depthFormat参数保存 + if (depthFormat == "png" || depthFormat == "both") { + // 保存PNG格式(8位) + 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图像:" << pngPath; + rightIRSuccess = true; + } + + if (depthFormat == "tiff" || depthFormat == "both") { + // 保存TIFF格式(保留16位精度) + QString tiffPath = QString("%1/%2_right_ir.tiff").arg(saveDir).arg(baseName); + cv::imwrite(tiffPath.toStdString(), rightIR16); + qDebug() << "保存右红外TIFF图像(16位):" << tiffPath; + rightIRSuccess = true; + } + } else { + qDebug() << "右红外数据大小不匹配:" << rightIRData.size(); + } + } 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 ? "成功" : "失败"); + << ", 点云:" << (cloudSuccess ? "成功" : "失败") + << ", 左红外:" << (leftIRSuccess ? "成功" : "失败") + << ", 右红外:" << (rightIRSuccess ? "成功" : "失败") + << ", RGB:" << (rgbSuccess ? "成功" : "失败"); } bool MainWindow::saveDepthImage(const QString &dir, const QString &baseName, const QString &format) @@ -1136,9 +1798,15 @@ void MainWindow::onSaveLogClicked() // ========== 统计信息更新 ========== void MainWindow::updateStatistics() { - // 更新红外图帧率 - if (m_depthFpsLabel) { - m_depthFpsLabel->setText(QString("红外图帧率: %1 fps").arg(m_currentDepthFps, 0, 'f', 1)); + // 更新三个相机的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)); } // 更新点云帧率 @@ -1146,10 +1814,12 @@ void MainWindow::updateStatistics() m_pointCloudFpsLabel->setText(QString("点云帧率: %1 fps").arg(m_currentPointCloudFps, 0, 'f', 1)); } - // 更新接收帧数(显示红外图和点云的累计总数) + // 更新接收帧数(显示三个相机和点云的累计总数) if (m_queueLabel) { - m_queueLabel->setText(QString("接收帧数: 红外%1 点云%2") - .arg(m_totalDepthFrameCount) + m_queueLabel->setText(QString("接收帧数: L%1 R%2 RGB%3 点云%4") + .arg(m_totalLeftFrameCount) + .arg(m_totalRightFrameCount) + .arg(m_totalRgbFrameCount) .arg(m_totalPointCloudFrameCount)); } } diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index 6b50c2a..9cd7a3d 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -35,7 +35,6 @@ private slots: // 相机控制槽函数 void onStartClicked(); void onStopClicked(); - void onOnceClicked(); void onExposureChanged(int value); void onCaptureClicked(); void onBrowseSavePathClicked(); @@ -51,6 +50,9 @@ private slots: // GVSP数据处理槽函数 void onImageReceived(const QImage &image, uint32_t blockId); + void onLeftImageReceived(const QByteArray &jpegData, uint32_t blockId); + void onRightImageReceived(const QByteArray &jpegData, uint32_t blockId); + void onRgbImageReceived(const QByteArray &jpegData, uint32_t blockId); void onDepthDataReceived(const QByteArray &depthData, uint32_t blockId); void onPointCloudDataReceived(const QByteArray &cloudData, uint32_t blockId); void onPointCloudReady(pcl::PointCloud::Ptr cloud, uint32_t blockId); @@ -84,7 +86,9 @@ private: bool savePointCloud(const QString &dir, const QString &baseName, const QString &format); void performBackgroundSave(const QString &saveDir, const QString &baseName, const QString &depthFormat, const QString &pointCloudFormat, - const QImage &image, pcl::PointCloud::Ptr cloud); + const QImage &image, pcl::PointCloud::Ptr cloud, + const QByteArray &leftIRData, const QByteArray &rightIRData, + const QByteArray &rgbData); // 日志辅助函数 void addLog(const QString &message, const QString &level = "INFO"); @@ -102,28 +106,45 @@ private: // 状态变量 bool m_isConnected; - bool m_autoSaveOnNextFrame; // 当前帧数据(用于拍照) QImage m_currentImage; pcl::PointCloud::Ptr m_currentPointCloud; uint32_t m_currentFrameId; + // 三个相机的原始JPEG数据(用于保存完整分辨率图像) + QByteArray m_currentLeftIRData; + QByteArray m_currentRightIRData; + QByteArray m_currentRgbData; + // UI控件指针 class QWidget *m_centralWidget; class QSplitter *m_mainSplitter; class QWidget *m_controlPanel; - class QLabel *m_imageDisplay; + class QLabel *m_imageDisplay; // 兼容旧代码 PointCloudWidget *m_pointCloudWidget; + + // 三个图像显示控件 + QLabel *m_leftImageDisplay; + QLabel *m_rightImageDisplay; + QLabel *m_rgbImageDisplay; // 按钮控件 QPushButton *m_refreshBtn; QPushButton *m_connectBtn; QPushButton *m_startBtn; QPushButton *m_stopBtn; - QPushButton *m_onceBtn; QPushButton *m_captureBtn; - + + // 数据传输开关按钮 + QPushButton *m_leftIRToggle; + QPushButton *m_rightIRToggle; + QPushButton *m_rgbToggle; + + // 单目/双目模式切换按钮 + QPushButton *m_monocularBtn; + QPushButton *m_binocularBtn; + // 输入控件 QSlider *m_exposureSlider; QSpinBox *m_exposureSpinBox; @@ -146,6 +167,11 @@ private: QLabel *m_resolutionLabel; QLabel *m_queueLabel; + // 三个相机的FPS标签 + QLabel *m_leftFpsLabel; + QLabel *m_rightFpsLabel; + QLabel *m_rgbFpsLabel; + // 日志显示控件 class QTextEdit *m_logDisplay; QPushButton *m_clearLogBtn; @@ -162,6 +188,33 @@ private: int m_pointCloudFrameCount; int m_totalPointCloudFrameCount; double m_currentPointCloudFps; + + // 统计数据 - 左红外 + QDateTime m_lastLeftFrameTime; + int m_leftFrameCount; + int m_totalLeftFrameCount; + double m_currentLeftFps; + + // 统计数据 - 右红外 + QDateTime m_lastRightFrameTime; + int m_rightFrameCount; + int m_totalRightFrameCount; + double m_currentRightFps; + + // 统计数据 - RGB + QDateTime m_lastRgbFrameTime; + int m_rgbFrameCount; + int m_totalRgbFrameCount; + double m_currentRgbFps; + + // RGB解码处理标志(防止线程积压) + QAtomicInt m_rgbProcessing; + int m_rgbSkipCounter; // RGB帧跳过计数器 + + // 相机启用状态标志(防止关闭后闪烁) + QAtomicInt m_leftIREnabled; + QAtomicInt m_rightIREnabled; + QAtomicInt m_rgbEnabled; }; #endif // MAINWINDOW_H diff --git a/src/gui/PointCloudGLWidget.cpp b/src/gui/PointCloudGLWidget.cpp index 3668a38..496b9e1 100644 --- a/src/gui/PointCloudGLWidget.cpp +++ b/src/gui/PointCloudGLWidget.cpp @@ -9,9 +9,11 @@ PointCloudGLWidget::PointCloudGLWidget(QWidget *parent) , m_vertexBuffer(nullptr) , m_vao(nullptr) , m_pointCount(0) + , m_fixedCenter(0.0f, 0.0f, 0.0f) + , m_centerInitialized(false) , m_orthoSize(2000.0f) // 正交投影视野大小 , m_rotationX(0.0f) // 从正面看(0度) - , m_rotationY(0.0f) // 从正面看(0度) + , m_rotationY(0.0f) // 不旋转Y轴 , m_translation(0.0f, 0.0f, 0.0f) , m_leftButtonPressed(false) , m_rightButtonPressed(false) @@ -67,7 +69,7 @@ void PointCloudGLWidget::setupShaders() uniform mat4 mvp; void main() { gl_Position = mvp * vec4(position, 1.0); - gl_PointSize = 2.0; + gl_PointSize = 1.0; // 减小点的大小 } )"; @@ -221,6 +223,27 @@ void PointCloudGLWidget::updatePointCloud(pcl::PointCloud::Ptr cl m_pointCount = m_vertices.size() / 3; + // 计算点云中心并进行中心化处理 + if (m_pointCount > 0) { + float centerX = (minX + maxX) / 2.0f; + float centerY = (minY + maxY) / 2.0f; + float centerZ = (minZ + maxZ) / 2.0f; + + // 第一帧:初始化固定中心点 + if (!m_centerInitialized) { + m_fixedCenter = QVector3D(centerX, centerY, centerZ); + m_centerInitialized = true; + qDebug() << "[PointCloudGLWidget] Fixed center initialized:" << m_fixedCenter; + } + + // 使用固定的中心点进行中心化(避免抖动) + for (size_t i = 0; i < m_vertices.size(); i += 3) { + m_vertices[i] -= m_fixedCenter.x(); // X坐标 + m_vertices[i + 1] -= m_fixedCenter.y(); // Y坐标 + m_vertices[i + 2] -= m_fixedCenter.z(); // Z坐标 + } + } + // 添加调试日志 static int updateCount = 0; if (updateCount < 3 || updateCount % 100 == 0) {