diff --git a/CMakeLists.txt b/CMakeLists.txt index fa07625..23bd149 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/README.md b/README.md index bcfa412..9362a98 100644 --- a/README.md +++ b/README.md @@ -167,16 +167,16 @@ C:\Program Files\D330Viewer\ - ✅ 网络配置(IP地址、端口设置) - ✅ 连接状态指示 - ✅ 配置持久化(QSettings) +- ✅ 点云颜色映射(深度着色) +- ✅ 多视角预设(正视、侧视、俯视) ### 🚧 当前开发计划 根据需求文档和用户反馈,后续待添加功能如下: - 录制功能(连续保存多帧) -- 点云颜色映射(深度着色) - 点云滤波选项(降噪、平滑) - 测量工具(距离、角度测量) -- 多视角预设(正视、侧视、俯视) - 性能监控(CPU/GPU使用率、内存使用) - 其他相机参数调节(增益、白平衡等) diff --git a/include/gui/PointCloudGLWidget.h b/include/gui/PointCloudGLWidget.h index 6219e22..a75a1ef 100644 --- a/include/gui/PointCloudGLWidget.h +++ b/include/gui/PointCloudGLWidget.h @@ -25,6 +25,7 @@ public: void updatePointCloud(pcl::PointCloud::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; diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp index ba7d584..1e219a1 100644 --- a/src/config/ConfigManager.cpp +++ b/src/config/ConfigManager.cpp @@ -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) diff --git a/src/core/DeviceScanner.cpp b/src/core/DeviceScanner.cpp index d2e45df..9164b9b 100644 --- a/src/core/DeviceScanner.cpp +++ b/src/core/DeviceScanner.cpp @@ -1,6 +1,8 @@ #include "DeviceScanner.h" #include #include +#include +#include #include 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 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; } diff --git a/src/core/DeviceScanner.h b/src/core/DeviceScanner.h index fa58009..d187d22 100644 --- a/src/core/DeviceScanner.h +++ b/src/core/DeviceScanner.h @@ -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 diff --git a/src/core/NetworkManager.cpp b/src/core/NetworkManager.cpp index 43490b7..c8ff492 100644 --- a/src/core/NetworkManager.cpp +++ b/src/core/NetworkManager.cpp @@ -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()); diff --git a/src/core/NetworkManager.h b/src/core/NetworkManager.h index aa9ff91..44112ec 100644 --- a/src/core/NetworkManager.h +++ b/src/core/NetworkManager.h @@ -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: diff --git a/src/core/PointCloudProcessor.cpp b/src/core/PointCloudProcessor.cpp index 1cfab1f..54ce3cd 100644 --- a/src/core/PointCloudProcessor.cpp +++ b/src/core/PointCloudProcessor.cpp @@ -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(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(cloudShort[i]) * m_zScale; - - // 正交投影:X、Y使用像素坐标(Y轴翻转以修正镜像) - cloud->points[i].x = static_cast(col); - cloud->points[i].y = static_cast(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(col); - cloud->points[i].y = static_cast(m_imageHeight - 1 - row); - cloud->points[i].z = static_cast(cloudShort[i * 3 + 2]) * m_zScale; + float z = static_cast(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; } } diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 0abd4d4..a8b0ffe 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -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); diff --git a/src/gui/PointCloudGLWidget.cpp b/src/gui/PointCloudGLWidget.cpp index 7c9636f..00bd57b 100644 --- a/src/gui/PointCloudGLWidget.cpp +++ b/src/gui/PointCloudGLWidget.cpp @@ -1,5 +1,6 @@ #include "gui/PointCloudGLWidget.h" #include +#include #include #include @@ -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::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::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) { diff --git a/src/main.cpp b/src/main.cpp index 9c4735f..d0c250f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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";