Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 04a5ec269f | |||
| 03c7cb58da | |||
| b1871aa9e7 |
@@ -1,5 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.15)
|
||||
project(D330Viewer VERSION 1.0.0 LANGUAGES CXX C)
|
||||
project(Viewer VERSION 1.0.0 LANGUAGES CXX C)
|
||||
|
||||
# 设置C++标准
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
@@ -142,24 +142,24 @@ install(DIRECTORY ${CMAKE_SOURCE_DIR}/bin/platforms/
|
||||
)
|
||||
|
||||
# ==================== CPack配置 - MSI安装程序 ====================
|
||||
set(CPACK_PACKAGE_NAME "D330Viewer")
|
||||
set(CPACK_PACKAGE_NAME "Viewer")
|
||||
set(CPACK_PACKAGE_VENDOR "Lorenzo Zhao")
|
||||
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "D330M Depth Camera Control System")
|
||||
set(CPACK_PACKAGE_VERSION "0.2.0")
|
||||
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Depth Camera Control System")
|
||||
set(CPACK_PACKAGE_VERSION "0.3.2")
|
||||
set(CPACK_PACKAGE_VERSION_MAJOR "0")
|
||||
set(CPACK_PACKAGE_VERSION_MINOR "2")
|
||||
set(CPACK_PACKAGE_VERSION_PATCH "0")
|
||||
set(CPACK_PACKAGE_INSTALL_DIRECTORY "D330Viewer")
|
||||
set(CPACK_PACKAGE_VERSION_MINOR "3")
|
||||
set(CPACK_PACKAGE_VERSION_PATCH "2")
|
||||
set(CPACK_PACKAGE_INSTALL_DIRECTORY "Viewer")
|
||||
|
||||
# WiX生成器配置(用于MSI)
|
||||
set(CPACK_GENERATOR "WIX")
|
||||
set(CPACK_WIX_UPGRADE_GUID "42365CB0-5840-487F-A2C8-56F9699A9022")
|
||||
set(CPACK_WIX_PROGRAM_MENU_FOLDER "D330Viewer")
|
||||
set(CPACK_WIX_PROGRAM_MENU_FOLDER "Viewer")
|
||||
set(CPACK_WIX_LICENSE_RTF "${CMAKE_SOURCE_DIR}/LICENSE.rtf")
|
||||
|
||||
# 创建开始菜单和桌面快捷方式
|
||||
set(CPACK_PACKAGE_EXECUTABLES "D330Viewer" "D330Viewer")
|
||||
set(CPACK_CREATE_DESKTOP_LINKS "D330Viewer")
|
||||
set(CPACK_PACKAGE_EXECUTABLES "Viewer" "Viewer")
|
||||
set(CPACK_CREATE_DESKTOP_LINKS "Viewer")
|
||||
|
||||
# 包含CPack模块
|
||||
include(CPack)
|
||||
|
||||
@@ -167,16 +167,16 @@ C:\Program Files\D330Viewer\
|
||||
- ✅ 网络配置(IP地址、端口设置)
|
||||
- ✅ 连接状态指示
|
||||
- ✅ 配置持久化(QSettings)
|
||||
- ✅ 点云颜色映射(深度着色)
|
||||
- ✅ 多视角预设(正视、侧视、俯视)
|
||||
|
||||
### 🚧 当前开发计划
|
||||
|
||||
根据需求文档和用户反馈,后续待添加功能如下:
|
||||
|
||||
- 录制功能(连续保存多帧)
|
||||
- 点云颜色映射(深度着色)
|
||||
- 点云滤波选项(降噪、平滑)
|
||||
- 测量工具(距离、角度测量)
|
||||
- 多视角预设(正视、侧视、俯视)
|
||||
- 性能监控(CPU/GPU使用率、内存使用)
|
||||
- 其他相机参数调节(增益、白平衡等)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ REM CMake配置脚本 - Windows版本
|
||||
REM 请根据实际安装路径修改以下变量
|
||||
|
||||
echo ========================================
|
||||
echo D330Viewer CMake配置脚本
|
||||
echo Viewer CMake配置脚本
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
@@ -47,7 +47,7 @@ if %ERRORLEVEL% EQU 0 (
|
||||
echo ========================================
|
||||
echo.
|
||||
echo 下一步:
|
||||
echo 1. 打开 build\D330Viewer.sln 使用Visual Studio编译
|
||||
echo 1. 打开 build\Viewer.sln 使用Visual Studio编译
|
||||
echo 2. 或运行: cmake --build build --config Release
|
||||
echo.
|
||||
) else (
|
||||
|
||||
@@ -22,11 +22,15 @@
|
||||
#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_MONO8_LEFT 0x01080006 // Mono8 format for left IR camera (downsampled)
|
||||
#define PIXEL_FORMAT_MONO8_RIGHT 0x01080007 // Mono8 format for right IR camera (downsampled)
|
||||
#define PIXEL_FORMAT_MJPEG 0x02180001 // MJPEG format for RGB camera
|
||||
|
||||
// Image dimensions
|
||||
#define IMAGE_WIDTH 1224
|
||||
#define IMAGE_HEIGHT 1024
|
||||
#define IR_DISPLAY_WIDTH 612 // Downsampled IR display width
|
||||
#define IR_DISPLAY_HEIGHT 512 // Downsampled IR display height
|
||||
#define RGB_WIDTH 1920
|
||||
#define RGB_HEIGHT 1080
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@ public:
|
||||
~PointCloudGLWidget();
|
||||
|
||||
void updatePointCloud(pcl::PointCloud<pcl::PointXYZ>::Ptr cloud);
|
||||
void setColorMode(bool enabled) { m_colorMode = enabled ? 1 : 0; update(); }
|
||||
bool colorMode() const { return m_colorMode != 0; }
|
||||
void resetView(); // 重置视角到初始状态
|
||||
|
||||
protected:
|
||||
void initializeGL() override;
|
||||
@@ -48,25 +51,31 @@ private:
|
||||
// 点云数据
|
||||
std::vector<float> m_vertices;
|
||||
int m_pointCount;
|
||||
|
||||
// 固定的点云中心点(避免抖动)
|
||||
QVector3D m_fixedCenter;
|
||||
bool m_centerInitialized;
|
||||
float m_minZ, m_maxZ; // 深度范围(用于着色)
|
||||
|
||||
// 相机参数
|
||||
QMatrix4x4 m_projection;
|
||||
QMatrix4x4 m_view;
|
||||
QMatrix4x4 m_model;
|
||||
|
||||
float m_orthoSize; // 正交投影视野大小(控制缩放)
|
||||
float m_fov; // 透视投影视场角
|
||||
float m_rotationX; // X轴旋转角度
|
||||
float m_rotationY; // Y轴旋转角度
|
||||
QVector3D m_translation; // 平移
|
||||
QVector3D m_cloudCenter; // 点云中心
|
||||
float m_viewDistance; // 观察距离
|
||||
QVector3D m_panOffset; // 用户平移偏移
|
||||
float m_zoom; // 缩放因子
|
||||
|
||||
// 鼠标交互状态
|
||||
QPoint m_lastMousePos;
|
||||
bool m_leftButtonPressed;
|
||||
bool m_rightButtonPressed;
|
||||
|
||||
// 首帧标志(只在首帧时自动居中)
|
||||
bool m_firstFrame;
|
||||
|
||||
// 颜色模式(0=黑白,1=彩色)
|
||||
int m_colorMode;
|
||||
};
|
||||
|
||||
#endif // POINTCLOUDGLWIDGET_H
|
||||
|
||||
@@ -19,6 +19,10 @@ public:
|
||||
// 更新点云显示
|
||||
void updatePointCloud(pcl::PointCloud<pcl::PointXYZ>::Ptr cloud);
|
||||
|
||||
// 颜色模式控制
|
||||
void setColorMode(bool enabled);
|
||||
bool colorMode() const;
|
||||
|
||||
private:
|
||||
QLabel *m_statusLabel;
|
||||
PointCloudGLWidget *m_glWidget;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "config/ConfigManager.h"
|
||||
|
||||
ConfigManager::ConfigManager()
|
||||
: m_settings(std::make_unique<QSettings>("D330Viewer", "D330Viewer"))
|
||||
: m_settings(std::make_unique<QSettings>("Viewer", "Viewer"))
|
||||
{
|
||||
// 构造函数:初始化QSettings
|
||||
}
|
||||
@@ -45,7 +45,7 @@ void ConfigManager::setDataPort(int port)
|
||||
// ========== 相机配置 ==========
|
||||
int ConfigManager::getExposureTime() const
|
||||
{
|
||||
return m_settings->value("Camera/ExposureTime", 10000).toInt();
|
||||
return m_settings->value("Camera/ExposureTime", 5980).toInt();
|
||||
}
|
||||
|
||||
void ConfigManager::setExposureTime(int exposure)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "DeviceScanner.h"
|
||||
#include <QNetworkDatagram>
|
||||
#include <QNetworkInterface>
|
||||
#include <QCoreApplication>
|
||||
#include <QThread>
|
||||
#include <QDebug>
|
||||
|
||||
DeviceScanner::DeviceScanner(QObject *parent)
|
||||
@@ -8,8 +10,9 @@ DeviceScanner::DeviceScanner(QObject *parent)
|
||||
, m_socket(new QUdpSocket(this))
|
||||
, m_timeoutTimer(new QTimer(this))
|
||||
, m_scanTimer(new QTimer(this))
|
||||
, m_currentHost(HOST_START)
|
||||
, m_totalHosts(HOST_END - HOST_START + 1)
|
||||
, m_prefixLength(24)
|
||||
, m_currentHost(0)
|
||||
, m_totalHosts(0)
|
||||
, m_isScanning(false)
|
||||
{
|
||||
connect(m_socket, &QUdpSocket::readyRead, this, &DeviceScanner::onReadyRead);
|
||||
@@ -33,8 +36,9 @@ void DeviceScanner::startScan(const QString &subnet)
|
||||
return;
|
||||
}
|
||||
|
||||
m_subnet = subnet.isEmpty() ? getLocalSubnet() : subnet;
|
||||
m_currentHost = HOST_START;
|
||||
// Get network info (base IP and prefix length)
|
||||
getLocalNetworkInfo(m_baseIp, m_prefixLength);
|
||||
|
||||
m_foundDevices.clear();
|
||||
m_isScanning = true;
|
||||
|
||||
@@ -44,17 +48,62 @@ void DeviceScanner::startScan(const QString &subnet)
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "Starting fast device scan on subnet:" << m_subnet;
|
||||
|
||||
// Send DISCOVER to all hosts at once (batch mode)
|
||||
for (int host = HOST_START; host <= HOST_END; host++) {
|
||||
QString ip = m_subnet + "." + QString::number(host);
|
||||
sendDiscoveryPacket(ip);
|
||||
// Calculate IP range based on prefix length
|
||||
QStringList parts = m_baseIp.split('.');
|
||||
if (parts.size() != 4) {
|
||||
emit scanError("Invalid base IP");
|
||||
m_isScanning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait 3 seconds for all responses
|
||||
m_timeoutTimer->start(3000);
|
||||
qDebug() << "Sent discovery packets to all hosts, waiting for responses...";
|
||||
quint32 baseAddr = (parts[0].toUInt() << 24) | (parts[1].toUInt() << 16) |
|
||||
(parts[2].toUInt() << 8) | parts[3].toUInt();
|
||||
quint32 mask = (0xFFFFFFFF << (32 - m_prefixLength)) & 0xFFFFFFFF;
|
||||
quint32 networkAddr = baseAddr & mask;
|
||||
quint32 broadcastAddr = networkAddr | (~mask & 0xFFFFFFFF);
|
||||
|
||||
qDebug() << "Starting device scan - Base IP:" << m_baseIp << "Prefix:" << m_prefixLength;
|
||||
|
||||
int packetsSent = 0;
|
||||
|
||||
// For large subnets (/16 or larger), use broadcast + batched unicast
|
||||
if (m_prefixLength <= 16) {
|
||||
// First: send to subnet broadcast address
|
||||
QHostAddress broadcast(broadcastAddr);
|
||||
qDebug() << "Large subnet detected, using broadcast discovery:" << broadcast.toString();
|
||||
sendDiscoveryPacket(broadcast.toString());
|
||||
packetsSent++;
|
||||
|
||||
// Second: scan all /24 subnets with throttling to avoid buffer overflow
|
||||
qDebug() << "Scanning all /24 subnets within /16 range (throttled)...";
|
||||
for (int oct3 = 0; oct3 <= 255; oct3++) {
|
||||
quint32 subNetBase = (networkAddr & 0xFFFF0000) | (oct3 << 8);
|
||||
for (int oct4 = 1; oct4 <= 254; oct4++) {
|
||||
quint32 addr = subNetBase | oct4;
|
||||
sendDiscoveryPacket(QHostAddress(addr).toString());
|
||||
packetsSent++;
|
||||
}
|
||||
// Process events and add small delay every /24 subnet to avoid buffer overflow
|
||||
QCoreApplication::processEvents();
|
||||
QThread::msleep(5);
|
||||
}
|
||||
} else {
|
||||
// For smaller subnets, scan all hosts
|
||||
qDebug() << "Network range:" << QHostAddress(networkAddr + 1).toString()
|
||||
<< "to" << QHostAddress(broadcastAddr - 1).toString();
|
||||
|
||||
for (quint32 addr = networkAddr + 1; addr < broadcastAddr; addr++) {
|
||||
sendDiscoveryPacket(QHostAddress(addr).toString());
|
||||
packetsSent++;
|
||||
}
|
||||
}
|
||||
|
||||
m_totalHosts = packetsSent;
|
||||
qDebug() << "Sent" << packetsSent << "discovery packets, waiting for responses...";
|
||||
|
||||
// Adjust timeout based on network size
|
||||
int timeout = (m_prefixLength >= 24) ? 3000 : 10000;
|
||||
m_timeoutTimer->start(timeout);
|
||||
}
|
||||
|
||||
void DeviceScanner::stopScan()
|
||||
@@ -91,10 +140,17 @@ void DeviceScanner::onReadyRead()
|
||||
|
||||
qDebug() << "Received response from" << senderIp << ":" << response;
|
||||
|
||||
if (response.contains("D330M_CAMERA")) {
|
||||
if (response.startsWith("EXPOSURE:")) {
|
||||
bool ok;
|
||||
int exposure = response.mid(9).trimmed().toInt(&ok);
|
||||
if (ok && exposure > 0) {
|
||||
qDebug() << "Received exposure from camera:" << exposure << "us";
|
||||
emit exposureReceived(exposure);
|
||||
}
|
||||
} else if (response.contains("D330M_CAMERA")) {
|
||||
DeviceInfo device;
|
||||
device.ipAddress = senderIp;
|
||||
device.deviceName = "D330M Camera";
|
||||
device.deviceName = "Camera";
|
||||
device.port = SCAN_PORT;
|
||||
device.responseTime = 0;
|
||||
|
||||
@@ -122,7 +178,7 @@ void DeviceScanner::sendDiscoveryPacket(const QString &ip)
|
||||
m_socket->writeDatagram(data, QHostAddress(ip), SCAN_PORT);
|
||||
}
|
||||
|
||||
QString DeviceScanner::getLocalSubnet()
|
||||
void DeviceScanner::getLocalNetworkInfo(QString &baseIp, int &prefixLength)
|
||||
{
|
||||
// Get all network interfaces
|
||||
QList<QNetworkInterface> interfaces = QNetworkInterface::allInterfaces();
|
||||
@@ -144,12 +200,14 @@ QString DeviceScanner::getLocalSubnet()
|
||||
for (const QNetworkAddressEntry &entry : entries) {
|
||||
QHostAddress addr = entry.ip();
|
||||
if (addr.protocol() == QAbstractSocket::IPv4Protocol && !addr.isLoopback()) {
|
||||
QString ip = addr.toString();
|
||||
QStringList parts = ip.split('.');
|
||||
if (parts.size() == 4) {
|
||||
qDebug() << "Found Ethernet adapter:" << iface.humanReadableName() << "IP:" << ip;
|
||||
return parts[0] + "." + parts[1] + "." + parts[2];
|
||||
baseIp = addr.toString();
|
||||
prefixLength = entry.prefixLength();
|
||||
if (prefixLength <= 0 || prefixLength > 32) {
|
||||
prefixLength = 24; // Default to /24 if invalid
|
||||
}
|
||||
qDebug() << "Found Ethernet adapter:" << iface.humanReadableName()
|
||||
<< "IP:" << baseIp << "Prefix:" << prefixLength;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,15 +227,19 @@ QString DeviceScanner::getLocalSubnet()
|
||||
for (const QNetworkAddressEntry &entry : entries) {
|
||||
QHostAddress addr = entry.ip();
|
||||
if (addr.protocol() == QAbstractSocket::IPv4Protocol && !addr.isLoopback()) {
|
||||
QString ip = addr.toString();
|
||||
QStringList parts = ip.split('.');
|
||||
if (parts.size() == 4) {
|
||||
qDebug() << "Found adapter:" << iface.humanReadableName() << "IP:" << ip;
|
||||
return parts[0] + "." + parts[1] + "." + parts[2];
|
||||
baseIp = addr.toString();
|
||||
prefixLength = entry.prefixLength();
|
||||
if (prefixLength <= 0 || prefixLength > 32) {
|
||||
prefixLength = 24;
|
||||
}
|
||||
qDebug() << "Found adapter:" << iface.humanReadableName()
|
||||
<< "IP:" << baseIp << "Prefix:" << prefixLength;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "192.168.0";
|
||||
// Default fallback
|
||||
baseIp = "192.168.0.1";
|
||||
prefixLength = 24;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ public:
|
||||
|
||||
signals:
|
||||
void deviceFound(const DeviceInfo &device);
|
||||
void exposureReceived(int exposureUs);
|
||||
void scanProgress(int current, int total);
|
||||
void scanFinished(int devicesFound);
|
||||
void scanError(const QString &error);
|
||||
@@ -40,13 +41,14 @@ private slots:
|
||||
|
||||
private:
|
||||
void sendDiscoveryPacket(const QString &ip);
|
||||
QString getLocalSubnet();
|
||||
void getLocalNetworkInfo(QString &baseIp, int &prefixLength);
|
||||
|
||||
QUdpSocket *m_socket;
|
||||
QTimer *m_timeoutTimer;
|
||||
QTimer *m_scanTimer;
|
||||
|
||||
QString m_subnet;
|
||||
QString m_baseIp;
|
||||
int m_prefixLength;
|
||||
int m_currentHost;
|
||||
int m_totalHosts;
|
||||
bool m_isScanning;
|
||||
@@ -55,8 +57,6 @@ private:
|
||||
|
||||
static constexpr int SCAN_PORT = 6790; // Control port for device discovery
|
||||
static constexpr int SCAN_TIMEOUT = 10;
|
||||
static constexpr int HOST_START = 1;
|
||||
static constexpr int HOST_END = 254;
|
||||
};
|
||||
|
||||
#endif // DEVICESCANNER_H
|
||||
|
||||
@@ -96,9 +96,9 @@ void GVSPParser::handleLeaderPacket(const uint8_t *data, size_t size)
|
||||
|
||||
// 根据像素格式选择对应的状态
|
||||
StreamState *state = nullptr;
|
||||
if (pixelFormat == PIXEL_FORMAT_MONO16_LEFT) {
|
||||
if (pixelFormat == PIXEL_FORMAT_MONO16_LEFT || pixelFormat == PIXEL_FORMAT_MONO8_LEFT) {
|
||||
state = &m_leftIRState;
|
||||
} else if (pixelFormat == PIXEL_FORMAT_MONO16_RIGHT) {
|
||||
} else if (pixelFormat == PIXEL_FORMAT_MONO16_RIGHT || pixelFormat == PIXEL_FORMAT_MONO8_RIGHT) {
|
||||
state = &m_rightIRState;
|
||||
} else if (pixelFormat == PIXEL_FORMAT_MJPEG) {
|
||||
state = &m_rgbState;
|
||||
@@ -118,6 +118,9 @@ void GVSPParser::handleLeaderPacket(const uint8_t *data, size_t size)
|
||||
if (pixelFormat == PIXEL_FORMAT_MJPEG) {
|
||||
// MJPEG是压缩格式,实际大小未知,设置为0表示动态接收
|
||||
state->expectedSize = 0;
|
||||
} else if (pixelFormat == PIXEL_FORMAT_MONO8_LEFT || pixelFormat == PIXEL_FORMAT_MONO8_RIGHT) {
|
||||
// 8-bit灰度格式(下采样)
|
||||
state->expectedSize = imageWidth * imageHeight;
|
||||
} else {
|
||||
// 16-bit或12-bit灰度等固定格式
|
||||
state->expectedSize = imageWidth * imageHeight * 2;
|
||||
@@ -268,6 +271,29 @@ void GVSPParser::processImageData(GVSPParser::StreamState *state)
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理Mono8格式(左右红外相机下采样8位数据)
|
||||
if (state->pixelFormat == PIXEL_FORMAT_MONO8_LEFT) {
|
||||
// 检查数据大小
|
||||
if (state->dataBuffer.size() < state->expectedSize) {
|
||||
return;
|
||||
}
|
||||
// 左红外8位数据
|
||||
emit leftImageReceived(state->dataBuffer, state->blockId);
|
||||
m_imageSequence++;
|
||||
return;
|
||||
}
|
||||
|
||||
if (state->pixelFormat == PIXEL_FORMAT_MONO8_RIGHT) {
|
||||
// 检查数据大小
|
||||
if (state->dataBuffer.size() < state->expectedSize) {
|
||||
return;
|
||||
}
|
||||
// 右红外8位数据
|
||||
emit rightImageReceived(state->dataBuffer, state->blockId);
|
||||
m_imageSequence++;
|
||||
return;
|
||||
}
|
||||
|
||||
// 兼容旧版本:使用序号区分(legacy)
|
||||
if (state->pixelFormat == PIXEL_FORMAT_MONO16) {
|
||||
// 检查数据大小
|
||||
|
||||
@@ -16,6 +16,9 @@ NetworkManager::NetworkManager(QObject *parent)
|
||||
connect(m_dataSocket, &QUdpSocket::readyRead, this, &NetworkManager::onReadyRead);
|
||||
connect(m_dataSocket, &QUdpSocket::errorOccurred, this, &NetworkManager::onError);
|
||||
|
||||
// 连接控制socket接收信号(用于接收相机回复,如曝光值)
|
||||
connect(m_controlSocket, &QUdpSocket::readyRead, this, &NetworkManager::onControlReadyRead);
|
||||
|
||||
// 连接GVSP解析器信号
|
||||
connect(m_gvspParser, &GVSPParser::imageReceived, this, &NetworkManager::imageReceived);
|
||||
connect(m_gvspParser, &GVSPParser::leftImageReceived, this, &NetworkManager::leftImageReceived);
|
||||
@@ -38,7 +41,7 @@ bool NetworkManager::connectToCamera(const QString &ip, int controlPort, int dat
|
||||
m_dataPort = dataPort;
|
||||
|
||||
// 绑定控制Socket到任意端口(让系统自动分配)
|
||||
if (!m_controlSocket->bind(QHostAddress::Any, 0)) {
|
||||
if(!m_controlSocket->bind(QHostAddress::Any, 0)) {
|
||||
QString error = QString("Failed to bind control socket: %1")
|
||||
.arg(m_controlSocket->errorString());
|
||||
qDebug() << error;
|
||||
@@ -48,7 +51,7 @@ bool NetworkManager::connectToCamera(const QString &ip, int controlPort, int dat
|
||||
qDebug() << "Successfully bound control socket to port" << m_controlSocket->localPort();
|
||||
|
||||
// 绑定数据接收端口
|
||||
if (!m_dataSocket->bind(QHostAddress::Any, m_dataPort)) {
|
||||
if(!m_dataSocket->bind(QHostAddress::Any, m_dataPort)) {
|
||||
QString error = QString("Failed to bind data port %1: %2")
|
||||
.arg(m_dataPort)
|
||||
.arg(m_dataSocket->errorString());
|
||||
@@ -67,6 +70,10 @@ bool NetworkManager::connectToCamera(const QString &ip, int controlPort, int dat
|
||||
m_isConnected = true;
|
||||
qDebug() << "Connected to camera:" << m_cameraIp << "Control port:" << m_controlPort << "Data port:" << m_dataPort;
|
||||
|
||||
// Send DISCOVER to get camera's current exposure value
|
||||
sendCommand("DISCOVER");
|
||||
qDebug() << "Sent DISCOVER to fetch camera exposure";
|
||||
|
||||
// Send STOP command to register client IP on camera
|
||||
sendStopCommand();
|
||||
qDebug() << "Sent STOP command to register client IP";
|
||||
@@ -77,7 +84,7 @@ bool NetworkManager::connectToCamera(const QString &ip, int controlPort, int dat
|
||||
|
||||
void NetworkManager::disconnectFromCamera()
|
||||
{
|
||||
if (m_isConnected) {
|
||||
if(m_isConnected) {
|
||||
m_controlSocket->close();
|
||||
m_dataSocket->close();
|
||||
m_isConnected = false;
|
||||
@@ -94,7 +101,7 @@ bool NetworkManager::isConnected() const
|
||||
// ========== 发送控制命令 ==========
|
||||
bool NetworkManager::sendCommand(const QString &command)
|
||||
{
|
||||
if (!m_isConnected) {
|
||||
if(!m_isConnected) {
|
||||
qDebug() << "Not connected to camera";
|
||||
return false;
|
||||
}
|
||||
@@ -109,7 +116,7 @@ bool NetworkManager::sendCommand(const QString &command)
|
||||
qint64 sent = m_controlSocket->writeDatagram(data, QHostAddress(m_cameraIp), m_controlPort);
|
||||
|
||||
qDebug() << "writeDatagram returned:" << sent;
|
||||
if (sent == -1) {
|
||||
if(sent == -1) {
|
||||
QString error = QString("Failed to send command: %1").arg(m_controlSocket->errorString());
|
||||
qDebug() << error;
|
||||
qDebug() << "Socket error code:" << m_controlSocket->error();
|
||||
@@ -134,23 +141,8 @@ bool NetworkManager::sendStopCommand()
|
||||
|
||||
bool NetworkManager::sendExposureCommand(int exposureTime)
|
||||
{
|
||||
// 同时发送结构光曝光命令(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;
|
||||
return sendCommand(exposureCommand);
|
||||
}
|
||||
|
||||
// ========== 传输开关命令 ==========
|
||||
@@ -208,7 +200,7 @@ void NetworkManager::onReadyRead()
|
||||
m_dataSocket->readDatagram(datagram.data(), datagram.size(), &sender, &senderPort);
|
||||
|
||||
// 只打印前5个包的详细信息
|
||||
if (packetCount < 5) {
|
||||
if(packetCount < 5) {
|
||||
// qDebug() << "[NetworkManager] Packet" << packetCount
|
||||
// << "from" << sender.toString() << ":" << senderPort
|
||||
// << "size:" << datagram.size() << "bytes";
|
||||
@@ -223,11 +215,32 @@ void NetworkManager::onReadyRead()
|
||||
}
|
||||
|
||||
// 每1000个包打印一次统计(减少日志量)
|
||||
if (packetCount % 1000 == 0) {
|
||||
if(packetCount % 1000 == 0) {
|
||||
// qDebug() << "[NetworkManager] Total packets received:" << packetCount;
|
||||
}
|
||||
}
|
||||
|
||||
void NetworkManager::onControlReadyRead()
|
||||
{
|
||||
while (m_controlSocket->hasPendingDatagrams()) {
|
||||
QByteArray datagram;
|
||||
datagram.resize(m_controlSocket->pendingDatagramSize());
|
||||
m_controlSocket->readDatagram(datagram.data(), datagram.size());
|
||||
|
||||
QString response = QString::fromUtf8(datagram);
|
||||
qDebug() << "[NetworkManager] Control response:" << response;
|
||||
|
||||
if (response.startsWith("EXPOSURE:")) {
|
||||
bool ok;
|
||||
int exposure = response.mid(9).trimmed().toInt(&ok);
|
||||
if (ok && exposure > 0) {
|
||||
qDebug() << "[NetworkManager] Camera exposure:" << exposure << "us";
|
||||
emit exposureReceived(exposure);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void NetworkManager::onError(QAbstractSocket::SocketError socketError)
|
||||
{
|
||||
QString error = QString("Socket error: %1").arg(m_dataSocket->errorString());
|
||||
|
||||
@@ -43,6 +43,7 @@ public:
|
||||
signals:
|
||||
void connected();
|
||||
void disconnected();
|
||||
void exposureReceived(int exposureUs);
|
||||
void errorOccurred(const QString &error);
|
||||
void dataReceived(const QByteArray &data);
|
||||
void imageReceived(const QImage &image, uint32_t blockId);
|
||||
@@ -54,6 +55,7 @@ signals:
|
||||
|
||||
private slots:
|
||||
void onReadyRead();
|
||||
void onControlReadyRead();
|
||||
void onError(QAbstractSocket::SocketError socketError);
|
||||
|
||||
private:
|
||||
|
||||
@@ -106,9 +106,8 @@ bool PointCloudProcessor::initializeOpenCL()
|
||||
"int y = idx / width; "
|
||||
"int x = idx % width; "
|
||||
"float z = depth[idx] * z_scale; "
|
||||
// 完全平面的圆柱投影:X和Y直接使用像素坐标,缩放到合适的范围
|
||||
"xyz[idx*3] = (x - cx) * 2.0f; " // X坐标,缩放系数2.0
|
||||
"xyz[idx*3+1] = -(y - cy) * 2.0f; " // Y坐标取反,修正上下颠倒
|
||||
"xyz[idx*3] = (x - cx) * z * inv_fx; "
|
||||
"xyz[idx*3+1] = (y - cy) * z * inv_fy; "
|
||||
"xyz[idx*3+2] = z; "
|
||||
"}";
|
||||
|
||||
@@ -283,31 +282,30 @@ void PointCloudProcessor::processPointCloudData(const QByteArray &cloudData, uin
|
||||
// 从int16_t数组读取点云数据
|
||||
const int16_t* cloudShort = reinterpret_cast<const int16_t*>(cloudData.constData());
|
||||
|
||||
float inv_fx = 1.0f / m_fx;
|
||||
float inv_fy = 1.0f / m_fy;
|
||||
|
||||
if (isZOnly) {
|
||||
// Z-only格式:转换为正交投影(柱形)
|
||||
// Z-only格式:标准针孔模型反投影
|
||||
for (size_t i = 0; i < m_totalPoints; i++) {
|
||||
int row = i / m_imageWidth;
|
||||
int col = i % m_imageWidth;
|
||||
|
||||
// 读取深度值(单位:毫米)
|
||||
float z = static_cast<float>(cloudShort[i]) * m_zScale;
|
||||
|
||||
// 正交投影:X、Y使用像素坐标(Y轴翻转以修正镜像)
|
||||
cloud->points[i].x = static_cast<float>(col);
|
||||
cloud->points[i].y = static_cast<float>(m_imageHeight - 1 - row);
|
||||
cloud->points[i].x = (col - m_cx) * z * inv_fx;
|
||||
cloud->points[i].y = (row - m_cy) * z * inv_fy;
|
||||
cloud->points[i].z = z;
|
||||
}
|
||||
} else {
|
||||
// XYZ格式:完整的三维坐标
|
||||
// 转换为正交投影(柱形),使用像素坐标作为X、Y
|
||||
// XYZ格式:使用Z值进行针孔模型反投影
|
||||
for (size_t i = 0; i < m_totalPoints; i++) {
|
||||
int row = i / m_imageWidth;
|
||||
int col = i % m_imageWidth;
|
||||
|
||||
// 正交投影:X、Y使用像素坐标(Y轴翻转以修正镜像),Z使用深度值
|
||||
cloud->points[i].x = static_cast<float>(col);
|
||||
cloud->points[i].y = static_cast<float>(m_imageHeight - 1 - row);
|
||||
cloud->points[i].z = static_cast<float>(cloudShort[i * 3 + 2]) * m_zScale;
|
||||
float z = static_cast<float>(cloudShort[i * 3 + 2]) * m_zScale;
|
||||
cloud->points[i].x = (col - m_cx) * z * inv_fx;
|
||||
cloud->points[i].y = (row - m_cy) * z * inv_fy;
|
||||
cloud->points[i].z = z;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
#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>())
|
||||
@@ -66,15 +66,17 @@ MainWindow::MainWindow(QWidget *parent)
|
||||
, m_rgbSkipCounter(0)
|
||||
{
|
||||
m_rgbProcessing.storeRelaxed(0); // 初始化RGB处理标志
|
||||
m_leftIREnabled.storeRelaxed(1); // 初始化左红外启用标志(默认启用)
|
||||
m_rightIREnabled.storeRelaxed(1); // 初始化右红外启用标志(默认启用)
|
||||
m_rgbEnabled.storeRelaxed(1); // 初始化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("D330Viewer 启动成功", "SUCCESS");
|
||||
addLog("Viewer 启动成功", "SUCCESS");
|
||||
addLog("等待连接相机...", "INFO");
|
||||
|
||||
// 启动UI更新定时器(100ms刷新一次)
|
||||
@@ -92,7 +94,7 @@ MainWindow::MainWindow(QWidget *parent)
|
||||
MainWindow::~MainWindow()
|
||||
{
|
||||
// 在退出前发送STOP命令,停止下位机采集
|
||||
if (m_isConnected && m_networkManager) {
|
||||
if(m_isConnected && m_networkManager) {
|
||||
qDebug() << "程序退出,发送STOP命令到下位机";
|
||||
m_networkManager->sendStopCommand();
|
||||
|
||||
@@ -105,7 +107,7 @@ MainWindow::~MainWindow()
|
||||
|
||||
void MainWindow::setupUI()
|
||||
{
|
||||
setWindowTitle("D330Viewer - D330M 相机控制");
|
||||
setWindowTitle("河北省科学院应用数学研究所");
|
||||
resize(1280, 720); // 16:9 比例
|
||||
|
||||
// 设置应用程序图标
|
||||
@@ -187,9 +189,9 @@ void MainWindow::setupUI()
|
||||
m_rightIRToggle->setCheckable(true);
|
||||
m_rgbToggle->setCheckable(true);
|
||||
|
||||
m_leftIRToggle->setChecked(true);
|
||||
m_rightIRToggle->setChecked(true);
|
||||
m_rgbToggle->setChecked(true);
|
||||
m_leftIRToggle->setChecked(false);
|
||||
m_rightIRToggle->setChecked(false);
|
||||
m_rgbToggle->setChecked(false);
|
||||
|
||||
m_leftIRToggle->setFixedHeight(32);
|
||||
m_rightIRToggle->setFixedHeight(32);
|
||||
@@ -219,6 +221,17 @@ void MainWindow::setupUI()
|
||||
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);
|
||||
|
||||
// 单目/双目模式切换按钮
|
||||
@@ -363,12 +376,12 @@ void MainWindow::setupUI()
|
||||
|
||||
QHBoxLayout *exposureLayout = new QHBoxLayout();
|
||||
m_exposureSlider = new QSlider(Qt::Horizontal, exposureGroup);
|
||||
m_exposureSlider->setRange(1000, 100000);
|
||||
m_exposureSlider->setValue(10000);
|
||||
m_exposureSlider->setRange(100, 100000);
|
||||
m_exposureSlider->setValue(5980);
|
||||
|
||||
m_exposureSpinBox = new QSpinBox(exposureGroup);
|
||||
m_exposureSpinBox->setRange(1000, 100000);
|
||||
m_exposureSpinBox->setValue(10000);
|
||||
m_exposureSpinBox->setRange(100, 100000);
|
||||
m_exposureSpinBox->setValue(5980);
|
||||
m_exposureSpinBox->setMinimumWidth(80);
|
||||
|
||||
exposureLayout->addWidget(m_exposureSlider, 3);
|
||||
@@ -626,6 +639,18 @@ void MainWindow::setupConnections()
|
||||
connect(m_networkManager.get(), &NetworkManager::disconnected, this, &MainWindow::onNetworkDisconnected);
|
||||
connect(m_networkManager.get(), &NetworkManager::dataReceived, this, &MainWindow::onDataReceived);
|
||||
|
||||
// 接收下位机曝光值并同步到UI(连接时触发)
|
||||
connect(m_networkManager.get(), &NetworkManager::exposureReceived, this, [this](int exposureUs) {
|
||||
qDebug() << "[MainWindow] 收到曝光值同步:" << exposureUs << "us";
|
||||
m_exposureSlider->blockSignals(true);
|
||||
m_exposureSpinBox->blockSignals(true);
|
||||
m_exposureSlider->setValue(exposureUs);
|
||||
m_exposureSpinBox->setValue(exposureUs);
|
||||
m_exposureSlider->blockSignals(false);
|
||||
m_exposureSpinBox->blockSignals(false);
|
||||
addLog(QString("同步相机曝光值: %1 μs").arg(exposureUs), "INFO");
|
||||
});
|
||||
|
||||
// GVSP数据信号连接(从NetworkManager)
|
||||
connect(m_networkManager.get(), &NetworkManager::imageReceived, this, &MainWindow::onImageReceived);
|
||||
connect(m_networkManager.get(), &NetworkManager::leftImageReceived, this, &MainWindow::onLeftImageReceived);
|
||||
@@ -645,6 +670,17 @@ void MainWindow::setupConnections()
|
||||
connect(m_deviceScanner.get(), &DeviceScanner::scanProgress, this, &MainWindow::onScanProgress);
|
||||
connect(m_deviceScanner.get(), &DeviceScanner::scanFinished, this, &MainWindow::onScanFinished);
|
||||
|
||||
// 接收下位机曝光值并同步到UI
|
||||
connect(m_deviceScanner.get(), &DeviceScanner::exposureReceived, this, [this](int exposureUs) {
|
||||
m_exposureSlider->blockSignals(true);
|
||||
m_exposureSpinBox->blockSignals(true);
|
||||
m_exposureSlider->setValue(exposureUs);
|
||||
m_exposureSpinBox->setValue(exposureUs);
|
||||
m_exposureSlider->blockSignals(false);
|
||||
m_exposureSpinBox->blockSignals(false);
|
||||
addLog(QString("同步相机曝光值: %1 μs").arg(exposureUs), "INFO");
|
||||
});
|
||||
|
||||
// 设备列表选择连接
|
||||
connect(m_deviceList, &QListWidget::itemClicked, this, &MainWindow::onDeviceSelected);
|
||||
|
||||
@@ -654,7 +690,7 @@ void MainWindow::setupConnections()
|
||||
|
||||
// 传输开关按钮连接
|
||||
connect(m_leftIRToggle, &QPushButton::toggled, this, [this](bool checked) {
|
||||
if (checked) {
|
||||
if(checked) {
|
||||
m_leftIREnabled.storeRelaxed(1); // 标记启用
|
||||
m_networkManager->sendEnableLeftIR();
|
||||
qDebug() << "启用左红外传输";
|
||||
@@ -663,7 +699,7 @@ void MainWindow::setupConnections()
|
||||
m_networkManager->sendDisableLeftIR();
|
||||
qDebug() << "禁用左红外传输";
|
||||
// 清除显示区域,恢复默认等待界面
|
||||
if (m_leftImageDisplay) {
|
||||
if(m_leftImageDisplay) {
|
||||
m_leftImageDisplay->setPixmap(QPixmap());
|
||||
m_leftImageDisplay->setText("左红外\n(等待数据...)");
|
||||
}
|
||||
@@ -671,7 +707,7 @@ void MainWindow::setupConnections()
|
||||
});
|
||||
|
||||
connect(m_rightIRToggle, &QPushButton::toggled, this, [this](bool checked) {
|
||||
if (checked) {
|
||||
if(checked) {
|
||||
m_rightIREnabled.storeRelaxed(1); // 标记启用
|
||||
m_networkManager->sendEnableRightIR();
|
||||
qDebug() << "启用右红外传输";
|
||||
@@ -680,7 +716,7 @@ void MainWindow::setupConnections()
|
||||
m_networkManager->sendDisableRightIR();
|
||||
qDebug() << "禁用右红外传输";
|
||||
// 清除显示区域,恢复默认等待界面
|
||||
if (m_rightImageDisplay) {
|
||||
if(m_rightImageDisplay) {
|
||||
m_rightImageDisplay->setPixmap(QPixmap());
|
||||
m_rightImageDisplay->setText("右红外\n(等待数据...)");
|
||||
}
|
||||
@@ -688,7 +724,7 @@ void MainWindow::setupConnections()
|
||||
});
|
||||
|
||||
connect(m_rgbToggle, &QPushButton::toggled, this, [this](bool checked) {
|
||||
if (checked) {
|
||||
if(checked) {
|
||||
m_rgbEnabled.storeRelaxed(1); // 标记启用
|
||||
m_networkManager->sendEnableRGB();
|
||||
qDebug() << "启用RGB传输";
|
||||
@@ -697,13 +733,21 @@ void MainWindow::setupConnections()
|
||||
m_networkManager->sendDisableRGB();
|
||||
qDebug() << "禁用RGB传输";
|
||||
// 清除显示区域,恢复默认等待界面
|
||||
if (m_rgbImageDisplay) {
|
||||
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);
|
||||
@@ -734,14 +778,14 @@ void MainWindow::loadSettings()
|
||||
// 加载深度图格式
|
||||
QString depthFormat = m_configManager->getDepthFormat();
|
||||
int depthIndex = m_depthFormatCombo->findData(depthFormat);
|
||||
if (depthIndex >= 0) {
|
||||
if(depthIndex >= 0) {
|
||||
m_depthFormatCombo->setCurrentIndex(depthIndex);
|
||||
}
|
||||
|
||||
// 加载点云格式
|
||||
QString pointCloudFormat = m_configManager->getPointCloudFormat();
|
||||
int pcIndex = m_pointCloudFormatCombo->findData(pointCloudFormat);
|
||||
if (pcIndex >= 0) {
|
||||
if(pcIndex >= 0) {
|
||||
m_pointCloudFormatCombo->setCurrentIndex(pcIndex);
|
||||
}
|
||||
}
|
||||
@@ -785,7 +829,7 @@ void MainWindow::onExposureChanged(int value)
|
||||
|
||||
void MainWindow::onConnectClicked()
|
||||
{
|
||||
if (m_isConnected) {
|
||||
if(m_isConnected) {
|
||||
m_networkManager->disconnectFromCamera();
|
||||
m_isConnected = false;
|
||||
qDebug() << "断开相机连接";
|
||||
@@ -794,7 +838,7 @@ void MainWindow::onConnectClicked()
|
||||
int ctrlPort = m_configManager->getControlPort();
|
||||
int dataPort = m_configManager->getDataPort();
|
||||
|
||||
if (m_networkManager->connectToCamera(ip, ctrlPort, dataPort)) {
|
||||
if(m_networkManager->connectToCamera(ip, ctrlPort, dataPort)) {
|
||||
m_isConnected = true;
|
||||
qDebug() << "连接到相机";
|
||||
}
|
||||
@@ -843,19 +887,19 @@ void MainWindow::onNetworkConnected()
|
||||
addLog(QString("已连接到相机: %1").arg(ip), "SUCCESS");
|
||||
|
||||
// 同步当前按钮状态到下位机
|
||||
if (m_leftIRToggle->isChecked()) {
|
||||
if(m_leftIRToggle->isChecked()) {
|
||||
m_networkManager->sendEnableLeftIR();
|
||||
} else {
|
||||
m_networkManager->sendDisableLeftIR();
|
||||
}
|
||||
|
||||
if (m_rightIRToggle->isChecked()) {
|
||||
if(m_rightIRToggle->isChecked()) {
|
||||
m_networkManager->sendEnableRightIR();
|
||||
} else {
|
||||
m_networkManager->sendDisableRightIR();
|
||||
}
|
||||
|
||||
if (m_rgbToggle->isChecked()) {
|
||||
if(m_rgbToggle->isChecked()) {
|
||||
m_networkManager->sendEnableRGB();
|
||||
} else {
|
||||
m_networkManager->sendDisableRGB();
|
||||
@@ -898,7 +942,7 @@ void MainWindow::onRefreshClicked()
|
||||
qDebug() << "刷新设备按钮点击";
|
||||
m_deviceList->clear();
|
||||
|
||||
if (m_deviceScanner->isScanning()) {
|
||||
if(m_deviceScanner->isScanning()) {
|
||||
m_deviceScanner->stopScan();
|
||||
m_refreshBtn->setText("刷新设备");
|
||||
} else {
|
||||
@@ -957,9 +1001,9 @@ void MainWindow::onImageReceived(const QImage &image, uint32_t blockId)
|
||||
m_depthFrameCount++;
|
||||
m_totalDepthFrameCount++;
|
||||
QDateTime currentTime = QDateTime::currentDateTime();
|
||||
if (m_lastDepthFrameTime.isValid()) {
|
||||
if(m_lastDepthFrameTime.isValid()) {
|
||||
qint64 elapsed = m_lastDepthFrameTime.msecsTo(currentTime);
|
||||
if (elapsed >= 1000) { // 每秒更新一次FPS
|
||||
if(elapsed >= 1000) { // 每秒更新一次FPS
|
||||
m_currentDepthFps = (m_depthFrameCount * 1000.0) / elapsed;
|
||||
m_depthFrameCount = 0;
|
||||
m_lastDepthFrameTime = currentTime;
|
||||
@@ -970,7 +1014,7 @@ void MainWindow::onImageReceived(const QImage &image, uint32_t blockId)
|
||||
}
|
||||
|
||||
// 将图像显示在UI上(使用快速缩放)
|
||||
if (m_imageDisplay) {
|
||||
if(m_imageDisplay) {
|
||||
QPixmap pixmap = QPixmap::fromImage(image);
|
||||
m_imageDisplay->setPixmap(pixmap.scaled(m_imageDisplay->size(), Qt::KeepAspectRatio, Qt::FastTransformation));
|
||||
}
|
||||
@@ -979,7 +1023,7 @@ void MainWindow::onImageReceived(const QImage &image, uint32_t blockId)
|
||||
void MainWindow::onLeftImageReceived(const QByteArray &jpegData, uint32_t blockId)
|
||||
{
|
||||
// 检查左红外是否启用,如果禁用则忽略数据(防止关闭后闪烁)
|
||||
if (m_leftIREnabled.loadAcquire() == 0) {
|
||||
if(m_leftIREnabled.loadAcquire() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -990,9 +1034,9 @@ void MainWindow::onLeftImageReceived(const QByteArray &jpegData, uint32_t blockI
|
||||
m_leftFrameCount++;
|
||||
m_totalLeftFrameCount++;
|
||||
QDateTime currentTime = QDateTime::currentDateTime();
|
||||
if (m_lastLeftFrameTime.isValid()) {
|
||||
if(m_lastLeftFrameTime.isValid()) {
|
||||
qint64 elapsed = m_lastLeftFrameTime.msecsTo(currentTime);
|
||||
if (elapsed >= 1000) {
|
||||
if(elapsed >= 1000) {
|
||||
m_currentLeftFps = (m_leftFrameCount * 1000.0) / elapsed;
|
||||
m_leftFrameCount = 0;
|
||||
m_lastLeftFrameTime = currentTime;
|
||||
@@ -1002,12 +1046,34 @@ void MainWindow::onLeftImageReceived(const QByteArray &jpegData, uint32_t blockI
|
||||
m_lastLeftFrameTime = currentTime;
|
||||
}
|
||||
|
||||
// 使用后台线程处理16位原始数据,避免阻塞UI
|
||||
if (m_leftImageDisplay && jpegData.size() > 0) {
|
||||
// 检查数据大小是否为16位图像
|
||||
size_t expectedSize = 1224 * 1024 * sizeof(uint16_t);
|
||||
if (jpegData.size() == expectedSize) {
|
||||
// 复制数据到局部变量
|
||||
// 使用后台线程处理红外数据,避免阻塞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;
|
||||
|
||||
// 在后台线程处理
|
||||
@@ -1022,23 +1088,23 @@ void MainWindow::onLeftImageReceived(const QByteArray &jpegData, uint32_t blockI
|
||||
// 第一遍:快速扫描找到粗略范围(每隔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(val > 0) {
|
||||
if(val < minVal) minVal = val;
|
||||
if(val > maxVal) maxVal = val;
|
||||
}
|
||||
}
|
||||
|
||||
// 第二遍:使用直方图统计精确百分位数(避免排序)
|
||||
if (maxVal > minVal) {
|
||||
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) {
|
||||
if(src[i] > 0) {
|
||||
int bin = (src[i] - minVal) / binWidth;
|
||||
if (bin >= histSize) bin = histSize - 1;
|
||||
if(bin >= histSize) bin = histSize - 1;
|
||||
histogram[bin]++;
|
||||
}
|
||||
}
|
||||
@@ -1053,10 +1119,10 @@ void MainWindow::onLeftImageReceived(const QByteArray &jpegData, uint32_t blockI
|
||||
int cumsum = 0;
|
||||
for (int i = 0; i < histSize; i++) {
|
||||
cumsum += histogram[i];
|
||||
if (cumsum >= thresh_1 && minVal == 65535) {
|
||||
if(cumsum >= thresh_1 && minVal == 65535) {
|
||||
minVal = minVal + i * binWidth;
|
||||
}
|
||||
if (cumsum >= thresh_99) {
|
||||
if(cumsum >= thresh_99) {
|
||||
maxVal = minVal + i * binWidth;
|
||||
break;
|
||||
}
|
||||
@@ -1069,11 +1135,11 @@ void MainWindow::onLeftImageReceived(const QByteArray &jpegData, uint32_t blockI
|
||||
float scale = (maxVal > minVal) ? (255.0f / (maxVal - minVal)) : 0.0f;
|
||||
|
||||
for (int i = 0; i < 1224 * 1024; i++) {
|
||||
if (src[i] == 0) {
|
||||
if(src[i] == 0) {
|
||||
dst[i] = 0;
|
||||
} else if (src[i] <= minVal) {
|
||||
} else if(src[i] <= minVal) {
|
||||
dst[i] = 0;
|
||||
} else if (src[i] >= maxVal) {
|
||||
} else if(src[i] >= maxVal) {
|
||||
dst[i] = 255;
|
||||
} else {
|
||||
dst[i] = static_cast<uint8_t>((src[i] - minVal) * scale);
|
||||
@@ -1084,7 +1150,7 @@ void MainWindow::onLeftImageReceived(const QByteArray &jpegData, uint32_t blockI
|
||||
|
||||
// 在主线程更新UI
|
||||
QMetaObject::invokeMethod(this, [this, imageCopy]() {
|
||||
if (m_leftImageDisplay) {
|
||||
if(m_leftImageDisplay) {
|
||||
QPixmap pixmap = QPixmap::fromImage(imageCopy);
|
||||
m_leftImageDisplay->setPixmap(pixmap.scaled(
|
||||
m_leftImageDisplay->size(), Qt::KeepAspectRatio, Qt::FastTransformation));
|
||||
@@ -1095,7 +1161,8 @@ void MainWindow::onLeftImageReceived(const QByteArray &jpegData, uint32_t blockI
|
||||
}
|
||||
});
|
||||
} else {
|
||||
qDebug() << "[MainWindow] ERROR: Left IR data size mismatch:" << jpegData.size();
|
||||
qDebug() << "[MainWindow] ERROR: Left IR data size mismatch:" << jpegData.size()
|
||||
<< "(expected 8bit:" << size8bit << "or 16bit:" << size16bit << ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1103,7 +1170,7 @@ void MainWindow::onLeftImageReceived(const QByteArray &jpegData, uint32_t blockI
|
||||
void MainWindow::onRightImageReceived(const QByteArray &jpegData, uint32_t blockId)
|
||||
{
|
||||
// 检查右红外是否启用,如果禁用则忽略数据(防止关闭后闪烁)
|
||||
if (m_rightIREnabled.loadAcquire() == 0) {
|
||||
if(m_rightIREnabled.loadAcquire() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1114,9 +1181,9 @@ void MainWindow::onRightImageReceived(const QByteArray &jpegData, uint32_t block
|
||||
m_rightFrameCount++;
|
||||
m_totalRightFrameCount++;
|
||||
QDateTime currentTime = QDateTime::currentDateTime();
|
||||
if (m_lastRightFrameTime.isValid()) {
|
||||
if(m_lastRightFrameTime.isValid()) {
|
||||
qint64 elapsed = m_lastRightFrameTime.msecsTo(currentTime);
|
||||
if (elapsed >= 1000) {
|
||||
if(elapsed >= 1000) {
|
||||
m_currentRightFps = (m_rightFrameCount * 1000.0) / elapsed;
|
||||
m_rightFrameCount = 0;
|
||||
m_lastRightFrameTime = currentTime;
|
||||
@@ -1126,12 +1193,34 @@ void MainWindow::onRightImageReceived(const QByteArray &jpegData, uint32_t block
|
||||
m_lastRightFrameTime = currentTime;
|
||||
}
|
||||
|
||||
// 使用后台线程处理16位原始数据,避免阻塞UI
|
||||
if (m_rightImageDisplay && jpegData.size() > 0) {
|
||||
// 检查数据大小是否为16位图像
|
||||
size_t expectedSize = 1224 * 1024 * sizeof(uint16_t);
|
||||
if (jpegData.size() == expectedSize) {
|
||||
// 复制数据到局部变量
|
||||
// 使用后台线程处理红外数据,避免阻塞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;
|
||||
|
||||
// 在后台线程处理
|
||||
@@ -1146,23 +1235,23 @@ void MainWindow::onRightImageReceived(const QByteArray &jpegData, uint32_t block
|
||||
// 第一遍:快速扫描找到粗略范围(每隔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(val > 0) {
|
||||
if(val < minVal) minVal = val;
|
||||
if(val > maxVal) maxVal = val;
|
||||
}
|
||||
}
|
||||
|
||||
// 第二遍:使用直方图统计精确百分位数(避免排序)
|
||||
if (maxVal > minVal) {
|
||||
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) {
|
||||
if(src[i] > 0) {
|
||||
int bin = (src[i] - minVal) / binWidth;
|
||||
if (bin >= histSize) bin = histSize - 1;
|
||||
if(bin >= histSize) bin = histSize - 1;
|
||||
histogram[bin]++;
|
||||
}
|
||||
}
|
||||
@@ -1177,10 +1266,10 @@ void MainWindow::onRightImageReceived(const QByteArray &jpegData, uint32_t block
|
||||
int cumsum = 0;
|
||||
for (int i = 0; i < histSize; i++) {
|
||||
cumsum += histogram[i];
|
||||
if (cumsum >= thresh_1 && minVal == 65535) {
|
||||
if(cumsum >= thresh_1 && minVal == 65535) {
|
||||
minVal = minVal + i * binWidth;
|
||||
}
|
||||
if (cumsum >= thresh_99) {
|
||||
if(cumsum >= thresh_99) {
|
||||
maxVal = minVal + i * binWidth;
|
||||
break;
|
||||
}
|
||||
@@ -1193,11 +1282,11 @@ void MainWindow::onRightImageReceived(const QByteArray &jpegData, uint32_t block
|
||||
float scale = (maxVal > minVal) ? (255.0f / (maxVal - minVal)) : 0.0f;
|
||||
|
||||
for (int i = 0; i < 1224 * 1024; i++) {
|
||||
if (src[i] == 0) {
|
||||
if(src[i] == 0) {
|
||||
dst[i] = 0;
|
||||
} else if (src[i] <= minVal) {
|
||||
} else if(src[i] <= minVal) {
|
||||
dst[i] = 0;
|
||||
} else if (src[i] >= maxVal) {
|
||||
} else if(src[i] >= maxVal) {
|
||||
dst[i] = 255;
|
||||
} else {
|
||||
dst[i] = static_cast<uint8_t>((src[i] - minVal) * scale);
|
||||
@@ -1208,7 +1297,7 @@ void MainWindow::onRightImageReceived(const QByteArray &jpegData, uint32_t block
|
||||
|
||||
// 在主线程更新UI
|
||||
QMetaObject::invokeMethod(this, [this, imageCopy]() {
|
||||
if (m_rightImageDisplay) {
|
||||
if(m_rightImageDisplay) {
|
||||
QPixmap pixmap = QPixmap::fromImage(imageCopy);
|
||||
m_rightImageDisplay->setPixmap(pixmap.scaled(
|
||||
m_rightImageDisplay->size(), Qt::KeepAspectRatio, Qt::FastTransformation));
|
||||
@@ -1219,7 +1308,8 @@ void MainWindow::onRightImageReceived(const QByteArray &jpegData, uint32_t block
|
||||
}
|
||||
});
|
||||
} else {
|
||||
qDebug() << "[MainWindow] ERROR: Right IR data size mismatch:" << jpegData.size();
|
||||
qDebug() << "[MainWindow] ERROR: Right IR data size mismatch:" << jpegData.size()
|
||||
<< "(expected 8bit:" << size8bit << "or 16bit:" << size16bit << ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1227,7 +1317,7 @@ void MainWindow::onRightImageReceived(const QByteArray &jpegData, uint32_t block
|
||||
void MainWindow::onRgbImageReceived(const QByteArray &jpegData, uint32_t blockId)
|
||||
{
|
||||
// 检查RGB是否启用,如果禁用则忽略数据(防止关闭后闪烁)
|
||||
if (m_rgbEnabled.loadAcquire() == 0) {
|
||||
if(m_rgbEnabled.loadAcquire() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1242,9 +1332,9 @@ void MainWindow::onRgbImageReceived(const QByteArray &jpegData, uint32_t blockId
|
||||
m_rgbFrameCount++;
|
||||
m_totalRgbFrameCount++;
|
||||
QDateTime currentTime = QDateTime::currentDateTime();
|
||||
if (m_lastRgbFrameTime.isValid()) {
|
||||
if(m_lastRgbFrameTime.isValid()) {
|
||||
qint64 elapsed = m_lastRgbFrameTime.msecsTo(currentTime);
|
||||
if (elapsed >= 1000) {
|
||||
if(elapsed >= 1000) {
|
||||
m_currentRgbFps = (m_rgbFrameCount * 1000.0) / elapsed;
|
||||
m_rgbFrameCount = 0;
|
||||
m_lastRgbFrameTime = currentTime;
|
||||
@@ -1257,9 +1347,9 @@ void MainWindow::onRgbImageReceived(const QByteArray &jpegData, uint32_t blockId
|
||||
// 移除频繁的日志输出
|
||||
|
||||
// 使用后台线程解码MJPEG,避免阻塞UI
|
||||
if (m_rgbImageDisplay && jpegData.size() > 0) {
|
||||
if(m_rgbImageDisplay && jpegData.size() > 0) {
|
||||
// 智能帧丢弃:如果上一帧还在处理,跳过当前帧(避免积压旧帧)
|
||||
if (m_rgbProcessing.loadAcquire() > 0) {
|
||||
if(m_rgbProcessing.loadAcquire() > 0) {
|
||||
return; // 跳过当前帧,优先处理最新帧
|
||||
}
|
||||
|
||||
@@ -1276,7 +1366,7 @@ void MainWindow::onRgbImageReceived(const QByteArray &jpegData, uint32_t blockId
|
||||
std::vector<uchar> buffer(dataCopy.begin(), dataCopy.end());
|
||||
cv::Mat cvImage = cv::imdecode(buffer, cv::IMREAD_COLOR);
|
||||
|
||||
if (!cvImage.empty()) {
|
||||
if(!cvImage.empty()) {
|
||||
// 缩放到1/2大小
|
||||
cv::Mat resized;
|
||||
cv::resize(cvImage, resized, cv::Size(), 0.5, 0.5, cv::INTER_LINEAR);
|
||||
@@ -1300,7 +1390,7 @@ void MainWindow::onRgbImageReceived(const QByteArray &jpegData, uint32_t blockId
|
||||
|
||||
// 使用DirectConnection直接在主线程更新UI(更快,避免队列积压)
|
||||
QMetaObject::invokeMethod(this, [this, pixmap]() {
|
||||
if (m_rgbImageDisplay) {
|
||||
if(m_rgbImageDisplay) {
|
||||
m_rgbImageDisplay->setPixmap(pixmap);
|
||||
}
|
||||
}, Qt::QueuedConnection);
|
||||
@@ -1347,9 +1437,9 @@ void MainWindow::onPointCloudReady(pcl::PointCloud<pcl::PointXYZ>::Ptr cloud, ui
|
||||
m_pointCloudFrameCount++;
|
||||
m_totalPointCloudFrameCount++;
|
||||
QDateTime currentTime = QDateTime::currentDateTime();
|
||||
if (m_lastPointCloudFrameTime.isValid()) {
|
||||
if(m_lastPointCloudFrameTime.isValid()) {
|
||||
qint64 elapsed = m_lastPointCloudFrameTime.msecsTo(currentTime);
|
||||
if (elapsed >= 1000) { // 每秒更新一次FPS
|
||||
if(elapsed >= 1000) { // 每秒更新一次FPS
|
||||
m_currentPointCloudFps = (m_pointCloudFrameCount * 1000.0) / elapsed;
|
||||
m_pointCloudFrameCount = 0;
|
||||
m_lastPointCloudFrameTime = currentTime;
|
||||
@@ -1360,7 +1450,7 @@ void MainWindow::onPointCloudReady(pcl::PointCloud<pcl::PointXYZ>::Ptr cloud, ui
|
||||
}
|
||||
|
||||
// 更新点云显示
|
||||
if (m_pointCloudWidget) {
|
||||
if(m_pointCloudWidget) {
|
||||
m_pointCloudWidget->updatePointCloud(cloud);
|
||||
}
|
||||
}
|
||||
@@ -1371,7 +1461,7 @@ void MainWindow::onCaptureClicked()
|
||||
qDebug() << "拍照按钮点击";
|
||||
|
||||
// 检查是否有可用数据
|
||||
if (m_currentImage.isNull() && (!m_currentPointCloud || m_currentPointCloud->empty())) {
|
||||
if(m_currentImage.isNull() && (!m_currentPointCloud || m_currentPointCloud->empty())) {
|
||||
QMessageBox::warning(this, "拍照失败", "没有可用的图像或点云数据。\n请先启动相机采集。");
|
||||
addLog("拍照失败:没有可用数据", "ERROR");
|
||||
return;
|
||||
@@ -1379,7 +1469,7 @@ void MainWindow::onCaptureClicked()
|
||||
|
||||
// 获取保存路径
|
||||
QString saveDir = m_savePathEdit->text().trimmed();
|
||||
if (saveDir.isEmpty()) {
|
||||
if(saveDir.isEmpty()) {
|
||||
// 使用默认路径:用户图片文件夹
|
||||
saveDir = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
|
||||
qDebug() << "使用默认保存路径:" << saveDir;
|
||||
@@ -1387,8 +1477,8 @@ void MainWindow::onCaptureClicked()
|
||||
|
||||
// 检查目录是否存在,不存在则创建
|
||||
QDir dir(saveDir);
|
||||
if (!dir.exists()) {
|
||||
if (!dir.mkpath(".")) {
|
||||
if(!dir.exists()) {
|
||||
if(!dir.mkpath(".")) {
|
||||
QMessageBox::warning(this, "拍照失败", QString("无法创建保存目录:\n%1").arg(saveDir));
|
||||
return;
|
||||
}
|
||||
@@ -1430,7 +1520,7 @@ void MainWindow::onBrowseSavePathClicked()
|
||||
? QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)
|
||||
: m_savePathEdit->text(),
|
||||
QFileDialog::ShowDirsOnly);
|
||||
if (!dir.isEmpty()) {
|
||||
if(!dir.isEmpty()) {
|
||||
m_savePathEdit->setText(dir);
|
||||
qDebug() << "设置保存路径:" << dir;
|
||||
}
|
||||
@@ -1448,14 +1538,14 @@ void MainWindow::performBackgroundSave(const QString &saveDir, const QString &ba
|
||||
bool cloudSuccess = false;
|
||||
|
||||
// 保存深度图
|
||||
if (!image.isNull()) {
|
||||
if(!image.isNull()) {
|
||||
try {
|
||||
// 转换QImage到OpenCV Mat
|
||||
cv::Mat mat;
|
||||
if (image.format() == QImage::Format_Grayscale8) {
|
||||
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) {
|
||||
} 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 {
|
||||
@@ -1466,10 +1556,10 @@ void MainWindow::performBackgroundSave(const QString &saveDir, const QString &ba
|
||||
}
|
||||
|
||||
// 根据format参数保存
|
||||
if (depthFormat == "png" || depthFormat == "both") {
|
||||
if(depthFormat == "png" || depthFormat == "both") {
|
||||
QString pngPath = QString("%1/%2_depth.png").arg(saveDir).arg(baseName);
|
||||
cv::Mat mat8bit;
|
||||
if (mat.type() == CV_16UC1) {
|
||||
if(mat.type() == CV_16UC1) {
|
||||
mat.convertTo(mat8bit, CV_8UC1, 255.0 / 65535.0);
|
||||
} else {
|
||||
mat8bit = mat;
|
||||
@@ -1479,9 +1569,9 @@ void MainWindow::performBackgroundSave(const QString &saveDir, const QString &ba
|
||||
depthSuccess = true;
|
||||
}
|
||||
|
||||
if (depthFormat == "tiff" || depthFormat == "both") {
|
||||
if(depthFormat == "tiff" || depthFormat == "both") {
|
||||
QString tiffPath = QString("%1/%2_depth.tiff").arg(saveDir).arg(baseName);
|
||||
if (mat.type() == CV_16UC1) {
|
||||
if(mat.type() == CV_16UC1) {
|
||||
cv::imwrite(tiffPath.toStdString(), mat);
|
||||
qDebug() << "保存TIFF深度图(16位):" << tiffPath;
|
||||
} else {
|
||||
@@ -1498,11 +1588,11 @@ void MainWindow::performBackgroundSave(const QString &saveDir, const QString &ba
|
||||
}
|
||||
|
||||
// 保存点云
|
||||
if (cloud && !cloud->empty()) {
|
||||
if(cloud && !cloud->empty()) {
|
||||
try {
|
||||
if (pointCloudFormat == "pcd" || pointCloudFormat == "both") {
|
||||
if(pointCloudFormat == "pcd" || pointCloudFormat == "both") {
|
||||
QString pcdPath = QString("%1/%2_pointcloud.pcd").arg(saveDir).arg(baseName);
|
||||
if (pcl::io::savePCDFileBinary(pcdPath.toStdString(), *cloud) == 0) {
|
||||
if(pcl::io::savePCDFileBinary(pcdPath.toStdString(), *cloud) == 0) {
|
||||
qDebug() << "保存PCD点云:" << pcdPath;
|
||||
cloudSuccess = true;
|
||||
} else {
|
||||
@@ -1510,9 +1600,9 @@ void MainWindow::performBackgroundSave(const QString &saveDir, const QString &ba
|
||||
}
|
||||
}
|
||||
|
||||
if (pointCloudFormat == "ply" || pointCloudFormat == "both") {
|
||||
if(pointCloudFormat == "ply" || pointCloudFormat == "both") {
|
||||
QString plyPath = QString("%1/%2_pointcloud.ply").arg(saveDir).arg(baseName);
|
||||
if (pcl::io::savePLYFileASCII(plyPath.toStdString(), *cloud) == 0) {
|
||||
if(pcl::io::savePLYFileASCII(plyPath.toStdString(), *cloud) == 0) {
|
||||
qDebug() << "保存PLY点云:" << plyPath;
|
||||
cloudSuccess = true;
|
||||
} else {
|
||||
@@ -1529,74 +1619,100 @@ void MainWindow::performBackgroundSave(const QString &saveDir, const QString &ba
|
||||
bool rightIRSuccess = false;
|
||||
bool rgbSuccess = false;
|
||||
|
||||
// 保存左红外图像(16位原始数据,1224×1024)
|
||||
if (!leftIRData.isEmpty()) {
|
||||
// 保存左红外图像(支持16位原始1224×1024或8位下采样612×512)
|
||||
if(!leftIRData.isEmpty()) {
|
||||
try {
|
||||
size_t expectedSize = 1224 * 1024 * sizeof(uint16_t);
|
||||
if (leftIRData.size() == expectedSize) {
|
||||
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());
|
||||
|
||||
// 创建16位灰度图像
|
||||
cv::Mat leftIR16(1024, 1224, CV_16UC1);
|
||||
memcpy(leftIR16.data, src, expectedSize);
|
||||
memcpy(leftIR16.data, src, size16bit);
|
||||
|
||||
// 根据depthFormat参数保存
|
||||
if (depthFormat == "png" || depthFormat == "both") {
|
||||
// 保存PNG格式(8位)
|
||||
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图像:" << pngPath;
|
||||
qDebug() << "保存左红外PNG图像(16bit):" << pngPath;
|
||||
leftIRSuccess = true;
|
||||
}
|
||||
|
||||
if (depthFormat == "tiff" || depthFormat == "both") {
|
||||
// 保存TIFF格式(保留16位精度)
|
||||
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();
|
||||
qDebug() << "左红外数据大小不匹配:" << leftIRData.size()
|
||||
<< "(期望16bit:" << size16bit << "或8bit:" << size8bit << ")";
|
||||
}
|
||||
} catch (const std::exception &e) {
|
||||
qDebug() << "保存左红外图像失败:" << e.what();
|
||||
}
|
||||
}
|
||||
|
||||
// 保存右红外图像(16位原始数据,1224×1024)
|
||||
if (!rightIRData.isEmpty()) {
|
||||
// 保存右红外图像(支持16位原始1224×1024或8位下采样612×512)
|
||||
if(!rightIRData.isEmpty()) {
|
||||
try {
|
||||
size_t expectedSize = 1224 * 1024 * sizeof(uint16_t);
|
||||
if (rightIRData.size() == expectedSize) {
|
||||
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());
|
||||
|
||||
// 创建16位灰度图像
|
||||
cv::Mat rightIR16(1024, 1224, CV_16UC1);
|
||||
memcpy(rightIR16.data, src, expectedSize);
|
||||
memcpy(rightIR16.data, src, size16bit);
|
||||
|
||||
// 根据depthFormat参数保存
|
||||
if (depthFormat == "png" || depthFormat == "both") {
|
||||
// 保存PNG格式(8位)
|
||||
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图像:" << pngPath;
|
||||
qDebug() << "保存右红外PNG图像(16bit):" << pngPath;
|
||||
rightIRSuccess = true;
|
||||
}
|
||||
|
||||
if (depthFormat == "tiff" || depthFormat == "both") {
|
||||
// 保存TIFF格式(保留16位精度)
|
||||
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();
|
||||
qDebug() << "右红外数据大小不匹配:" << rightIRData.size()
|
||||
<< "(期望16bit:" << size16bit << "或8bit:" << size8bit << ")";
|
||||
}
|
||||
} catch (const std::exception &e) {
|
||||
qDebug() << "保存右红外图像失败:" << e.what();
|
||||
@@ -1604,12 +1720,12 @@ void MainWindow::performBackgroundSave(const QString &saveDir, const QString &ba
|
||||
}
|
||||
|
||||
// 保存RGB图像(MJPEG原始数据,完整分辨率1920×1080)
|
||||
if (!rgbData.isEmpty()) {
|
||||
if(!rgbData.isEmpty()) {
|
||||
try {
|
||||
// 直接保存JPEG文件(无需解码,保持原始质量)
|
||||
QString rgbPath = QString("%1/%2_rgb.jpg").arg(saveDir).arg(baseName);
|
||||
QFile file(rgbPath);
|
||||
if (file.open(QIODevice::WriteOnly)) {
|
||||
if(file.open(QIODevice::WriteOnly)) {
|
||||
file.write(rgbData);
|
||||
file.close();
|
||||
qDebug() << "保存RGB图像(JPEG):" << rgbPath;
|
||||
@@ -1634,10 +1750,10 @@ bool MainWindow::saveDepthImage(const QString &dir, const QString &baseName, con
|
||||
try {
|
||||
// 转换QImage到OpenCV Mat
|
||||
cv::Mat mat;
|
||||
if (m_currentImage.format() == QImage::Format_Grayscale8) {
|
||||
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) {
|
||||
} 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 {
|
||||
@@ -1648,11 +1764,11 @@ bool MainWindow::saveDepthImage(const QString &dir, const QString &baseName, con
|
||||
}
|
||||
|
||||
// 根据format参数保存
|
||||
if (format == "png" || format == "both") {
|
||||
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) {
|
||||
if(mat.type() == CV_16UC1) {
|
||||
mat.convertTo(mat8bit, CV_8UC1, 255.0 / 65535.0);
|
||||
} else {
|
||||
mat8bit = mat;
|
||||
@@ -1661,10 +1777,10 @@ bool MainWindow::saveDepthImage(const QString &dir, const QString &baseName, con
|
||||
qDebug() << "保存PNG深度图:" << pngPath;
|
||||
}
|
||||
|
||||
if (format == "tiff" || format == "both") {
|
||||
if(format == "tiff" || format == "both") {
|
||||
// 保存TIFF格式(16位原始数据)
|
||||
QString tiffPath = QString("%1/%2_depth.tiff").arg(dir).arg(baseName);
|
||||
if (mat.type() == CV_16UC1) {
|
||||
if(mat.type() == CV_16UC1) {
|
||||
cv::imwrite(tiffPath.toStdString(), mat);
|
||||
qDebug() << "保存TIFF深度图(16位):" << tiffPath;
|
||||
} else {
|
||||
@@ -1686,7 +1802,7 @@ bool MainWindow::saveDepthImage(const QString &dir, const QString &baseName, con
|
||||
bool MainWindow::savePointCloud(const QString &dir, const QString &baseName, const QString &format)
|
||||
{
|
||||
try {
|
||||
if (!m_currentPointCloud || m_currentPointCloud->empty()) {
|
||||
if(!m_currentPointCloud || m_currentPointCloud->empty()) {
|
||||
qDebug() << "点云数据为空";
|
||||
return false;
|
||||
}
|
||||
@@ -1694,10 +1810,10 @@ bool MainWindow::savePointCloud(const QString &dir, const QString &baseName, con
|
||||
bool success = false;
|
||||
|
||||
// 根据format参数保存
|
||||
if (format == "pcd" || format == "both") {
|
||||
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) {
|
||||
if(pcl::io::savePCDFileBinary(pcdPath.toStdString(), *m_currentPointCloud) == 0) {
|
||||
qDebug() << "保存PCD点云:" << pcdPath;
|
||||
success = true;
|
||||
} else {
|
||||
@@ -1705,10 +1821,10 @@ bool MainWindow::savePointCloud(const QString &dir, const QString &baseName, con
|
||||
}
|
||||
}
|
||||
|
||||
if (format == "ply" || format == "both") {
|
||||
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) {
|
||||
if(pcl::io::savePLYFileASCII(plyPath.toStdString(), *m_currentPointCloud) == 0) {
|
||||
qDebug() << "保存PLY点云:" << plyPath;
|
||||
success = true;
|
||||
} else {
|
||||
@@ -1726,18 +1842,18 @@ bool MainWindow::savePointCloud(const QString &dir, const QString &baseName, con
|
||||
// ========== 日志功能 ==========
|
||||
void MainWindow::addLog(const QString &message, const QString &level)
|
||||
{
|
||||
if (!m_logDisplay) return;
|
||||
if(!m_logDisplay) return;
|
||||
|
||||
// 获取当前时间戳
|
||||
QString timestamp = QDateTime::currentDateTime().toString("HH:mm:ss.zzz");
|
||||
|
||||
// 根据日志级别设置颜色
|
||||
QString color;
|
||||
if (level == "ERROR") {
|
||||
if(level == "ERROR") {
|
||||
color = "#FF6B6B"; // 红色
|
||||
} else if (level == "WARNING") {
|
||||
} else if(level == "WARNING") {
|
||||
color = "#FFA500"; // 橙色
|
||||
} else if (level == "SUCCESS") {
|
||||
} else if(level == "SUCCESS") {
|
||||
color = "#4CAF50"; // 绿色
|
||||
} else {
|
||||
color = "#D4D4D4"; // 默认灰白色
|
||||
@@ -1758,7 +1874,7 @@ void MainWindow::addLog(const QString &message, const QString &level)
|
||||
|
||||
// 限制日志行数(保留最近1000行)
|
||||
QTextDocument *doc = m_logDisplay->document();
|
||||
if (doc->blockCount() > 1000) {
|
||||
if(doc->blockCount() > 1000) {
|
||||
QTextCursor cursor = m_logDisplay->textCursor();
|
||||
cursor.movePosition(QTextCursor::Start);
|
||||
cursor.movePosition(QTextCursor::Down, QTextCursor::KeepAnchor, doc->blockCount() - 1000);
|
||||
@@ -1771,7 +1887,7 @@ void MainWindow::addLog(const QString &message, const QString &level)
|
||||
|
||||
void MainWindow::onClearLogClicked()
|
||||
{
|
||||
if (m_logDisplay) {
|
||||
if(m_logDisplay) {
|
||||
m_logDisplay->clear();
|
||||
addLog("日志已清除", "INFO");
|
||||
}
|
||||
@@ -1780,11 +1896,11 @@ void MainWindow::onClearLogClicked()
|
||||
void MainWindow::onSaveLogClicked()
|
||||
{
|
||||
QString fileName = QFileDialog::getSaveFileName(this, "保存日志",
|
||||
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/d330viewer_log.txt",
|
||||
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/viewer_log.txt",
|
||||
"文本文件 (*.txt);;所有文件 (*.*)");
|
||||
if (!fileName.isEmpty()) {
|
||||
if(!fileName.isEmpty()) {
|
||||
QFile file(fileName);
|
||||
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
if(file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
QTextStream out(&file);
|
||||
out << m_logDisplay->toPlainText();
|
||||
file.close();
|
||||
@@ -1799,23 +1915,23 @@ void MainWindow::onSaveLogClicked()
|
||||
void MainWindow::updateStatistics()
|
||||
{
|
||||
// 更新三个相机的FPS
|
||||
if (m_leftFpsLabel) {
|
||||
if(m_leftFpsLabel) {
|
||||
m_leftFpsLabel->setText(QString("左红外: %1 fps").arg(m_currentLeftFps, 0, 'f', 1));
|
||||
}
|
||||
if (m_rightFpsLabel) {
|
||||
if(m_rightFpsLabel) {
|
||||
m_rightFpsLabel->setText(QString("右红外: %1 fps").arg(m_currentRightFps, 0, 'f', 1));
|
||||
}
|
||||
if (m_rgbFpsLabel) {
|
||||
if(m_rgbFpsLabel) {
|
||||
m_rgbFpsLabel->setText(QString("RGB彩色: %1 fps").arg(m_currentRgbFps, 0, 'f', 1));
|
||||
}
|
||||
|
||||
// 更新点云帧率
|
||||
if (m_pointCloudFpsLabel) {
|
||||
if(m_pointCloudFpsLabel) {
|
||||
m_pointCloudFpsLabel->setText(QString("点云帧率: %1 fps").arg(m_currentPointCloudFps, 0, 'f', 1));
|
||||
}
|
||||
|
||||
// 更新接收帧数(显示三个相机和点云的累计总数)
|
||||
if (m_queueLabel) {
|
||||
if(m_queueLabel) {
|
||||
m_queueLabel->setText(QString("接收帧数: L%1 R%2 RGB%3 点云%4")
|
||||
.arg(m_totalLeftFrameCount)
|
||||
.arg(m_totalRightFrameCount)
|
||||
@@ -1827,7 +1943,7 @@ void MainWindow::updateStatistics()
|
||||
// 监听系统主题变化事件
|
||||
void MainWindow::changeEvent(QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::PaletteChange) {
|
||||
if(event->type() == QEvent::PaletteChange) {
|
||||
// 系统调色板变化,说明主题可能已切换
|
||||
qDebug() << "系统主题已变化,刷新UI样式";
|
||||
|
||||
|
||||
@@ -140,6 +140,7 @@ private:
|
||||
QPushButton *m_leftIRToggle;
|
||||
QPushButton *m_rightIRToggle;
|
||||
QPushButton *m_rgbToggle;
|
||||
QPushButton *m_pointCloudColorToggle; // 点云颜色开关
|
||||
|
||||
// 单目/双目模式切换按钮
|
||||
QPushButton *m_monocularBtn;
|
||||
@@ -207,8 +208,10 @@ private:
|
||||
int m_totalRgbFrameCount;
|
||||
double m_currentRgbFps;
|
||||
|
||||
// RGB解码处理标志(防止线程积压)
|
||||
// 解码处理标志(防止线程积压导致闪烁)
|
||||
QAtomicInt m_rgbProcessing;
|
||||
QAtomicInt m_leftIRProcessing;
|
||||
QAtomicInt m_rightIRProcessing;
|
||||
int m_rgbSkipCounter; // RGB帧跳过计数器
|
||||
|
||||
// 相机启用状态标志(防止关闭后闪烁)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "gui/PointCloudGLWidget.h"
|
||||
#include <QDebug>
|
||||
#include <QPushButton>
|
||||
#include <cmath>
|
||||
#include <cfloat>
|
||||
|
||||
@@ -9,16 +10,32 @@ 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_minZ(0.0f)
|
||||
, m_maxZ(1000.0f)
|
||||
, m_fov(60.0f) // 透视投影视场角
|
||||
, m_rotationX(0.0f) // 从正面看(0度)
|
||||
, m_rotationY(0.0f) // 不旋转Y轴
|
||||
, m_translation(0.0f, 0.0f, 0.0f)
|
||||
, m_cloudCenter(0.0f, 0.0f, 0.0f)
|
||||
, m_viewDistance(1000.0f)
|
||||
, m_panOffset(0.0f, 0.0f, 0.0f)
|
||||
, m_zoom(1.0f) // 缩放因子
|
||||
, m_leftButtonPressed(false)
|
||||
, m_rightButtonPressed(false)
|
||||
, m_firstFrame(true)
|
||||
, m_colorMode(0) // 默认黑白模式
|
||||
{
|
||||
setMinimumSize(400, 400);
|
||||
|
||||
// 添加重置视角按钮
|
||||
QPushButton *resetBtn = new QPushButton("重置", this);
|
||||
resetBtn->setFixedSize(60, 30);
|
||||
resetBtn->move(10, 10);
|
||||
resetBtn->setStyleSheet(
|
||||
"QPushButton { background-color: rgba(50, 50, 50, 180); color: white; border: 1px solid #555; border-radius: 4px; }"
|
||||
"QPushButton:hover { background-color: rgba(70, 70, 70, 200); }"
|
||||
"QPushButton:pressed { background-color: rgba(40, 40, 40, 220); }"
|
||||
);
|
||||
connect(resetBtn, &QPushButton::clicked, this, &PointCloudGLWidget::resetView);
|
||||
}
|
||||
|
||||
PointCloudGLWidget::~PointCloudGLWidget()
|
||||
@@ -42,7 +59,7 @@ void PointCloudGLWidget::initializeGL()
|
||||
{
|
||||
initializeOpenGLFunctions();
|
||||
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClearColor(0.1f, 0.1f, 0.15f, 1.0f); // 深灰色背景
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glEnable(GL_PROGRAM_POINT_SIZE);
|
||||
|
||||
@@ -67,18 +84,39 @@ void PointCloudGLWidget::setupShaders()
|
||||
#version 330 core
|
||||
layout(location = 0) in vec3 position;
|
||||
uniform mat4 mvp;
|
||||
uniform float minZ;
|
||||
uniform float maxZ;
|
||||
out float depth;
|
||||
void main() {
|
||||
gl_Position = mvp * vec4(position, 1.0);
|
||||
gl_PointSize = 1.0; // 减小点的大小
|
||||
gl_PointSize = 1.0;
|
||||
depth = (position.z - minZ) / (maxZ - minZ);
|
||||
}
|
||||
)";
|
||||
|
||||
// 片段着色器
|
||||
// 片段着色器 - 支持黑白和彩色两种模式
|
||||
const char *fragmentShaderSource = R"(
|
||||
#version 330 core
|
||||
in float depth;
|
||||
uniform int colorMode;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
fragColor = vec4(1.0, 1.0, 1.0, 1.0);
|
||||
float d = clamp(depth, 0.0, 1.0);
|
||||
if (colorMode == 0) {
|
||||
fragColor = vec4(1.0, 1.0, 1.0, 1.0);
|
||||
} else {
|
||||
vec3 color;
|
||||
if (d < 0.25) {
|
||||
color = mix(vec3(0.0, 0.0, 1.0), vec3(0.0, 1.0, 1.0), d * 4.0);
|
||||
} else if (d < 0.5) {
|
||||
color = mix(vec3(0.0, 1.0, 1.0), vec3(0.0, 1.0, 0.0), (d - 0.25) * 4.0);
|
||||
} else if (d < 0.75) {
|
||||
color = mix(vec3(0.0, 1.0, 0.0), vec3(1.0, 1.0, 0.0), (d - 0.5) * 4.0);
|
||||
} else {
|
||||
color = mix(vec3(1.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), (d - 0.75) * 4.0);
|
||||
}
|
||||
fragColor = vec4(color, 1.0);
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
@@ -99,11 +137,12 @@ void PointCloudGLWidget::resizeGL(int w, int h)
|
||||
{
|
||||
m_projection.setToIdentity();
|
||||
|
||||
// 使用正交投影代替透视投影,避免"喷射状"效果
|
||||
// 使用正交投影,避免透视变形
|
||||
float aspect = float(w) / float(h);
|
||||
m_projection.ortho(-m_orthoSize * aspect, m_orthoSize * aspect,
|
||||
-m_orthoSize, m_orthoSize,
|
||||
-50000.0f, 50000.0f); // 近平面和远平面
|
||||
float orthoSize = m_viewDistance * 0.5f / m_zoom;
|
||||
m_projection.ortho(-orthoSize * aspect, orthoSize * aspect,
|
||||
-orthoSize, orthoSize,
|
||||
-50000.0f, 50000.0f);
|
||||
}
|
||||
|
||||
void PointCloudGLWidget::paintGL()
|
||||
@@ -114,18 +153,25 @@ void PointCloudGLWidget::paintGL()
|
||||
return;
|
||||
}
|
||||
|
||||
// 每帧重新计算正交投影矩阵(确保使用最新的m_orthoSize)
|
||||
// 重新计算正交投影矩阵
|
||||
m_projection.setToIdentity();
|
||||
float aspect = float(width()) / float(height());
|
||||
m_projection.ortho(-m_orthoSize * aspect, m_orthoSize * aspect,
|
||||
-m_orthoSize, m_orthoSize,
|
||||
float orthoSize = m_viewDistance * 0.5f / m_zoom;
|
||||
m_projection.ortho(-orthoSize * aspect, orthoSize * aspect,
|
||||
-orthoSize, orthoSize,
|
||||
-50000.0f, 50000.0f);
|
||||
|
||||
// 设置view矩阵
|
||||
// 设置view矩阵 - 轨道相机模式(围绕点云中心旋转)
|
||||
m_view.setToIdentity();
|
||||
// 1. 用户平移偏移
|
||||
m_view.translate(m_panOffset);
|
||||
// 2. 相机后退到观察距离
|
||||
m_view.translate(0.0f, 0.0f, -m_viewDistance);
|
||||
// 3. 应用旋转(围绕原点,即点云中心)
|
||||
m_view.rotate(m_rotationX, 1.0f, 0.0f, 0.0f);
|
||||
m_view.rotate(m_rotationY, 0.0f, 1.0f, 0.0f);
|
||||
m_view.translate(m_translation);
|
||||
// 4. 将点云中心移到原点
|
||||
m_view.translate(-m_cloudCenter);
|
||||
|
||||
// 设置model矩阵
|
||||
m_model.setToIdentity();
|
||||
@@ -136,6 +182,9 @@ void PointCloudGLWidget::paintGL()
|
||||
// 绑定shader和设置uniform
|
||||
m_program->bind();
|
||||
m_program->setUniformValue("mvp", mvp);
|
||||
m_program->setUniformValue("minZ", m_minZ);
|
||||
m_program->setUniformValue("maxZ", m_maxZ);
|
||||
m_program->setUniformValue("colorMode", m_colorMode);
|
||||
|
||||
// 绑定VAO和绘制
|
||||
m_vao->bind();
|
||||
@@ -166,10 +215,10 @@ void PointCloudGLWidget::mouseMoveEvent(QMouseEvent *event)
|
||||
m_rotationY += delta.x() * 0.5f;
|
||||
update();
|
||||
} else if (m_rightButtonPressed) {
|
||||
// 右键:平移(根据正交投影视野大小调整平移速度)
|
||||
float scale = m_orthoSize * 0.002f;
|
||||
m_translation.setX(m_translation.x() + delta.x() * scale);
|
||||
m_translation.setY(m_translation.y() - delta.y() * scale);
|
||||
// 右键:平移(根据观察距离调整平移速度)
|
||||
float scale = m_viewDistance * 0.002f;
|
||||
m_panOffset.setX(m_panOffset.x() + delta.x() * scale);
|
||||
m_panOffset.setY(m_panOffset.y() - delta.y() * scale);
|
||||
update();
|
||||
}
|
||||
}
|
||||
@@ -185,12 +234,12 @@ void PointCloudGLWidget::mouseReleaseEvent(QMouseEvent *event)
|
||||
|
||||
void PointCloudGLWidget::wheelEvent(QWheelEvent *event)
|
||||
{
|
||||
// 滚轮:缩放(调整正交投影视野大小)
|
||||
// 滚轮:缩放(调整zoom因子)
|
||||
float delta = event->angleDelta().y() / 120.0f;
|
||||
m_orthoSize *= (1.0f - delta * 0.1f);
|
||||
m_orthoSize = qMax(100.0f, qMin(m_orthoSize, 10000.0f)); // 范围:100-10000
|
||||
m_zoom *= (1.0f + delta * 0.1f);
|
||||
m_zoom = qMax(0.1f, qMin(m_zoom, 10.0f)); // 范围:0.1-10倍
|
||||
|
||||
update(); // 触发重绘,paintGL会使用新的m_orthoSize
|
||||
update();
|
||||
}
|
||||
|
||||
void PointCloudGLWidget::updatePointCloud(pcl::PointCloud<pcl::PointXYZ>::Ptr cloud)
|
||||
@@ -199,19 +248,20 @@ void PointCloudGLWidget::updatePointCloud(pcl::PointCloud<pcl::PointXYZ>::Ptr cl
|
||||
return;
|
||||
}
|
||||
|
||||
// 过滤全零点并转换为顶点数组
|
||||
// 过滤全零点并转换为顶点数组,同时计算包围盒
|
||||
m_vertices.clear();
|
||||
|
||||
float minX = FLT_MAX, maxX = -FLT_MAX;
|
||||
float minY = FLT_MAX, maxY = -FLT_MAX;
|
||||
float minZ = FLT_MAX, maxZ = -FLT_MAX;
|
||||
|
||||
for (const auto& point : cloud->points) {
|
||||
if (point.z > 0.01f) {
|
||||
if (point.z > 0.01f) { // 过滤掉无效的零点
|
||||
m_vertices.push_back(point.x);
|
||||
m_vertices.push_back(point.y);
|
||||
m_vertices.push_back(-point.y);
|
||||
m_vertices.push_back(point.z);
|
||||
|
||||
// 统计坐标范围
|
||||
// 更新包围盒
|
||||
if (point.x < minX) minX = point.x;
|
||||
if (point.x > maxX) maxX = point.x;
|
||||
if (point.y < minY) minY = point.y;
|
||||
@@ -223,42 +273,61 @@ void PointCloudGLWidget::updatePointCloud(pcl::PointCloud<pcl::PointXYZ>::Ptr cl
|
||||
|
||||
m_pointCount = m_vertices.size() / 3;
|
||||
|
||||
// 计算点云中心并进行中心化处理
|
||||
// 保存深度范围用于着色
|
||||
if (m_pointCount > 0) {
|
||||
m_minZ = minZ;
|
||||
m_maxZ = maxZ;
|
||||
}
|
||||
|
||||
// 只在首帧时自动调整相机位置,之后保持用户交互状态
|
||||
if (m_pointCount > 0 && m_firstFrame) {
|
||||
// 计算点云中心
|
||||
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;
|
||||
}
|
||||
// 计算点云尺寸
|
||||
float depthRange = maxZ - minZ;
|
||||
float sizeX = maxX - minX;
|
||||
float sizeY = maxY - minY;
|
||||
float maxSize = std::max({sizeX, sizeY, depthRange});
|
||||
|
||||
// 使用固定的中心点进行中心化(避免抖动)
|
||||
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坐标
|
||||
}
|
||||
}
|
||||
// 设置点云中心
|
||||
m_cloudCenter = QVector3D(centerX, centerY, centerZ);
|
||||
|
||||
// 添加调试日志
|
||||
static int updateCount = 0;
|
||||
if (updateCount < 3 || updateCount % 100 == 0) {
|
||||
// qDebug() << "[PointCloudGLWidget] Update" << updateCount << "- Points:" << m_pointCount
|
||||
// << "Total cloud size:" << cloud->size();
|
||||
// qDebug() << " X range:" << minX << "to" << maxX;
|
||||
// qDebug() << " Y range:" << minY << "to" << maxY;
|
||||
// qDebug() << " Z range:" << minZ << "to" << maxZ;
|
||||
// 计算观察距离,让相机从外部观察点云
|
||||
m_viewDistance = maxSize * 1.5f;
|
||||
|
||||
// 重置平移偏移和旋转角度
|
||||
m_panOffset = QVector3D(0.0f, 0.0f, 0.0f);
|
||||
m_rotationX = 0.0f;
|
||||
m_rotationY = 0.0f;
|
||||
|
||||
// 设置缩放
|
||||
m_zoom = 1.0f;
|
||||
|
||||
qDebug() << "[PointCloudGLWidget] 首帧自动居中 - 点云中心:" << centerX << centerY << centerZ
|
||||
<< "观察距离:" << m_viewDistance;
|
||||
m_firstFrame = false; // 标记首帧已处理
|
||||
}
|
||||
updateCount++;
|
||||
|
||||
updateBuffers();
|
||||
update();
|
||||
}
|
||||
|
||||
void PointCloudGLWidget::resetView()
|
||||
{
|
||||
// 重置所有视角参数到初始状态
|
||||
m_rotationX = 0.0f;
|
||||
m_rotationY = 0.0f;
|
||||
m_panOffset = QVector3D(0.0f, 0.0f, 0.0f);
|
||||
m_zoom = 1.0f;
|
||||
m_firstFrame = true; // 标记为首帧,下次更新时会重新计算视角
|
||||
|
||||
update();
|
||||
qDebug() << "[PointCloudGLWidget] 视角已重置";
|
||||
}
|
||||
|
||||
void PointCloudGLWidget::updateBuffers()
|
||||
{
|
||||
if (m_vertices.empty() || !m_vao || !m_vertexBuffer) {
|
||||
|
||||
@@ -54,3 +54,15 @@ void PointCloudWidget::updatePointCloud(pcl::PointCloud<pcl::PointXYZ>::Ptr clou
|
||||
// 更新OpenGL显示
|
||||
m_glWidget->updatePointCloud(cloud);
|
||||
}
|
||||
|
||||
void PointCloudWidget::setColorMode(bool enabled)
|
||||
{
|
||||
if (m_glWidget) {
|
||||
m_glWidget->setColorMode(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
bool PointCloudWidget::colorMode() const
|
||||
{
|
||||
return m_glWidget ? m_glWidget->colorMode() : false;
|
||||
}
|
||||
|
||||
12
src/main.cpp
12
src/main.cpp
@@ -31,19 +31,19 @@ int main(int argc, char *argv[])
|
||||
QApplication app(argc, argv);
|
||||
|
||||
// 设置应用程序信息
|
||||
app.setOrganizationName("D330Viewer");
|
||||
app.setApplicationName("D330Viewer");
|
||||
app.setApplicationVersion("0.2.0");
|
||||
app.setOrganizationName("Viewer");
|
||||
app.setApplicationName("Viewer");
|
||||
app.setApplicationVersion("0.3.2");
|
||||
|
||||
// 初始化Logger(在可执行文件同目录下)
|
||||
QString logPath = QCoreApplication::applicationDirPath() + "/d330viewer.log";
|
||||
QString logPath = QCoreApplication::applicationDirPath() + "/viewer.log";
|
||||
Logger::instance()->setLogFile(logPath);
|
||||
Logger::instance()->setMaxLines(10000); // 保留最新10000行
|
||||
|
||||
// 安装消息处理器
|
||||
qInstallMessageHandler(messageHandler);
|
||||
|
||||
qDebug() << "D330Viewer started";
|
||||
qDebug() << "Viewer started";
|
||||
qDebug() << "Log file:" << logPath;
|
||||
|
||||
// 创建并显示主窗口
|
||||
@@ -52,7 +52,7 @@ int main(int argc, char *argv[])
|
||||
|
||||
int result = app.exec();
|
||||
|
||||
qDebug() << "D330Viewer exiting";
|
||||
qDebug() << "Viewer exiting";
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user