update: 扩大ip搜索范围,修改点云可视化逻辑,添加上位机与下位机曝光值同步

This commit is contained in:
2026-02-06 14:47:42 +08:00
parent 03c7cb58da
commit 04a5ec269f
12 changed files with 204 additions and 59 deletions

View File

@@ -145,10 +145,10 @@ install(DIRECTORY ${CMAKE_SOURCE_DIR}/bin/platforms/
set(CPACK_PACKAGE_NAME "Viewer")
set(CPACK_PACKAGE_VENDOR "Lorenzo Zhao")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Depth Camera Control System")
set(CPACK_PACKAGE_VERSION "0.3.0")
set(CPACK_PACKAGE_VERSION "0.3.2")
set(CPACK_PACKAGE_VERSION_MAJOR "0")
set(CPACK_PACKAGE_VERSION_MINOR "3")
set(CPACK_PACKAGE_VERSION_PATCH "0")
set(CPACK_PACKAGE_VERSION_PATCH "2")
set(CPACK_PACKAGE_INSTALL_DIRECTORY "Viewer")
# WiX生成器配置用于MSI

View File

@@ -167,16 +167,16 @@ C:\Program Files\D330Viewer\
- ✅ 网络配置IP地址、端口设置
- ✅ 连接状态指示
- ✅ 配置持久化QSettings
- ✅ 点云颜色映射(深度着色)
- ✅ 多视角预设(正视、侧视、俯视)
### 🚧 当前开发计划
根据需求文档和用户反馈,后续待添加功能如下:
- 录制功能(连续保存多帧)
- 点云颜色映射(深度着色)
- 点云滤波选项(降噪、平滑)
- 测量工具(距离、角度测量)
- 多视角预设(正视、侧视、俯视)
- 性能监控CPU/GPU使用率、内存使用
- 其他相机参数调节(增益、白平衡等)

View File

@@ -25,6 +25,7 @@ public:
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;

View File

@@ -45,7 +45,7 @@ void ConfigManager::setDataPort(int port)
// ========== 相机配置 ==========
int ConfigManager::getExposureTime() const
{
return m_settings->value("Camera/ExposureTime", 1000).toInt();
return m_settings->value("Camera/ExposureTime", 5980).toInt();
}
void ConfigManager::setExposureTime(int exposure)

View File

@@ -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,7 +140,14 @@ 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 = "Camera";
@@ -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;
}

View File

@@ -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

View File

@@ -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);
@@ -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";
@@ -213,6 +220,27 @@ void NetworkManager::onReadyRead()
}
}
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());

View File

@@ -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:

View File

@@ -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;
}
}

View File

@@ -377,11 +377,11 @@ void MainWindow::setupUI()
QHBoxLayout *exposureLayout = new QHBoxLayout();
m_exposureSlider = new QSlider(Qt::Horizontal, exposureGroup);
m_exposureSlider->setRange(100, 100000);
m_exposureSlider->setValue(1000);
m_exposureSlider->setValue(5980);
m_exposureSpinBox = new QSpinBox(exposureGroup);
m_exposureSpinBox->setRange(100, 100000);
m_exposureSpinBox->setValue(1000);
m_exposureSpinBox->setValue(5980);
m_exposureSpinBox->setMinimumWidth(80);
exposureLayout->addWidget(m_exposureSlider, 3);
@@ -639,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);
@@ -658,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);

View File

@@ -1,5 +1,6 @@
#include "gui/PointCloudGLWidget.h"
#include <QDebug>
#include <QPushButton>
#include <cmath>
#include <cfloat>
@@ -24,6 +25,17 @@ PointCloudGLWidget::PointCloudGLWidget(QWidget *parent)
, 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()
@@ -125,9 +137,12 @@ void PointCloudGLWidget::resizeGL(int w, int h)
{
m_projection.setToIdentity();
// 使用透视投影,从相机原点看向Z轴正方向
// 使用正交投影,避免透视变形
float aspect = float(w) / float(h);
m_projection.perspective(m_fov, aspect, 1.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()
@@ -138,10 +153,13 @@ void PointCloudGLWidget::paintGL()
return;
}
// 重新计算透视投影矩阵
// 重新计算正交投影矩阵
m_projection.setToIdentity();
float aspect = float(width()) / float(height());
m_projection.perspective(m_fov / m_zoom, aspect, 1.0f, 50000.0f);
float orthoSize = m_viewDistance * 0.5f / m_zoom;
m_projection.ortho(-orthoSize * aspect, orthoSize * aspect,
-orthoSize, orthoSize,
-50000.0f, 50000.0f);
// 设置view矩阵 - 轨道相机模式(围绕点云中心旋转)
m_view.setToIdentity();
@@ -238,9 +256,9 @@ void PointCloudGLWidget::updatePointCloud(pcl::PointCloud<pcl::PointXYZ>::Ptr cl
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);
// 更新包围盒
@@ -297,6 +315,19 @@ void PointCloudGLWidget::updatePointCloud(pcl::PointCloud<pcl::PointXYZ>::Ptr cl
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) {

View File

@@ -33,7 +33,7 @@ int main(int argc, char *argv[])
// 设置应用程序信息
app.setOrganizationName("Viewer");
app.setApplicationName("Viewer");
app.setApplicationVersion("0.3.0");
app.setApplicationVersion("0.3.2");
// 初始化Logger在可执行文件同目录下
QString logPath = QCoreApplication::applicationDirPath() + "/viewer.log";