refactor: 重构项目构建系统与点云查看器功能

1.  新增CloudUtils静态库与相关CMake配置
2.  重构VrUtils构建脚本,修正编译选项与依赖
3.  为Windows平台添加utf-8编译选项
4.  扩展PointCloudGLWidget功能:新增点云移除、选择信息叠加、增量相机适配
5.  优化点云添加接口,支持不重置相机视图
6.  修正激光数据保存时的DataType输出
7.  为Point3D结构体添加单位注释
8.  调整鼠标交互逻辑,统一按键操作行为
This commit is contained in:
杰仔 2026-08-18 12:26:28 +08:00
parent 44180b7f85
commit 9271a37a81
8 changed files with 427 additions and 185 deletions

View File

@ -1,16 +1,23 @@
cmake_minimum_required(VERSION 3.14)
project(WoodCamUtils LANGUAGES CXX)
project(WoodCamUtils LANGUAGES C CXX)
option(WOODCAM_BUILD_CLOUDVIEW3D "Build the Qt/OpenGL CloudView3D utility" ON)
option(WOODCAM_BUILD_CLOUDUTILS "Build the CloudUtils static library" ON)
# VrCommon is a header-oriented compatibility module. Keep it as an interface
# target so consumers get the canonical VZNL_Types.h include path.
add_library(VrCommon INTERFACE)
target_include_directories(VrCommon INTERFACE
if(NOT TARGET VrCommon)
add_library(VrCommon INTERFACE)
target_include_directories(VrCommon INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/VrCommon/Inc
)
)
endif()
if(WOODCAM_BUILD_CLOUDVIEW3D)
add_subdirectory(CloudView3D)
endif()
if(WOODCAM_BUILD_CLOUDUTILS)
add_subdirectory(CloudUtils)
endif()

46
CloudUtils/CMakeLists.txt Normal file
View File

@ -0,0 +1,46 @@
cmake_minimum_required(VERSION 3.14)
find_package(Qt5 REQUIRED COMPONENTS Core Gui)
if(NOT TARGET VrCommon)
add_library(VrCommon INTERFACE)
target_include_directories(VrCommon INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/../VrCommon/Inc
)
endif()
if(NOT TARGET VrUtils)
add_subdirectory(
"${CMAKE_CURRENT_SOURCE_DIR}/../VrUtils"
"${CMAKE_CURRENT_BINARY_DIR}/VrUtils"
)
endif()
add_library(CloudUtils STATIC
Src/LaserDataLoader.cpp
Src/PointCloudImageUtils.cpp
Inc/LaserDataLoader.h
Inc/PointCloudImageUtils.h
)
set_target_properties(CloudUtils PROPERTIES
CXX_STANDARD 11
CXX_STANDARD_REQUIRED ON
)
target_include_directories(CloudUtils PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/Inc
)
target_link_libraries(CloudUtils
PUBLIC
VrCommon
Qt5::Core
Qt5::Gui
PRIVATE
VrUtils
)
if(MSVC)
target_compile_options(CloudUtils PRIVATE /utf-8)
endif()

View File

@ -546,7 +546,7 @@ int LaserDataLoader::SaveLaserScanData(const std::string& fileName,
// 写入文件头
sw << "LineNum:" << lineNum << '\n';
sw << "DataType:" << static_cast<int>(fileDataType) << '\n';
sw << "DataType:" << 0 << '\n';
sw << "ScanSpeed:" << scanSpeed << '\n';
sw << "PointAdjust: 1" << '\n';
sw << "MaxTimeStamp:" << maxTimeStamp << "_" << clockPerSecond << '\n';

View File

@ -36,6 +36,7 @@ target_link_libraries(CloudView3D PUBLIC
)
if(WIN32)
target_compile_options(CloudView3D PRIVATE /utf-8)
target_link_libraries(CloudView3D PUBLIC opengl32 glu32)
elseif(UNIX AND NOT APPLE)
target_link_libraries(CloudView3D PUBLIC GL GLU)

View File

@ -7,14 +7,14 @@
struct Point3D
{
float x, y, z;
float x, y, z; // millimetres
Point3D() : x(0), y(0), z(0) {}
Point3D(float _x, float _y, float _z) : x(_x), y(_y), z(_z) {}
};
struct Point3DRGB
{
float x, y, z;
float x, y, z; // millimetres
uint8_t r, g, b;
float pointSize;

View File

@ -11,6 +11,7 @@
#include <QMouseEvent>
#include <QWheelEvent>
#include <vector>
#include <deque>
#include "CloudView3DTypes.h"
#include "VZNL_Types.h"
@ -175,8 +176,10 @@ public:
~PointCloudGLWidget() override;
void addPointCloud(const PointCloudXYZ& cloud, const QString& name = "");
void addPointCloud(const PointCloudXYZRGB& cloud, const QString& name = "");
void addPointCloud(const PointCloudXYZRGB& cloud, const QString& name = "",
bool resetCamera = true);
void appendPointCloud(const PointCloudXYZRGB& cloud, const QString& name = "");
void removeOldestPointCloud();
void clearPointClouds();
void setPointCloudColor(PointCloudColor color);
void setPointSize(float size);
@ -480,6 +483,7 @@ private:
void drawLineSegmentList(const QVector<LineSegment>& segments);
void drawPosePoints(); // 绘制姿态点
void drawAxisLabels(); // 绘制坐标轴 XYZ 标注
void drawSelectionInfoOverlay(); // 右上角显示选点和测量结果
void uploadToVBO(PointCloudData& data); // 上传数据到 VBO
void releaseVBO(PointCloudData& data); // 释放 VBO 资源
@ -504,7 +508,9 @@ private:
void updateViewAngleDisplay();
bool handlePinchGesture(QPinchGesture* gesture);
std::vector<PointCloudData> m_pointClouds;
// Each entry owns QOpenGLBuffer objects. deque avoids relocating existing
// entries when new scan lines are appended or the oldest one is removed.
std::deque<PointCloudData> m_pointClouds;
QMatrix4x4 m_projection;
QMatrix4x4 m_view;

View File

@ -7,11 +7,13 @@
#include <QFormLayout>
#include <QGestureEvent>
#include <QHBoxLayout>
#include <QFontMetrics>
#include <QOpenGLContext>
#include <QPainter>
#include <QPinchGesture>
#include <QPushButton>
#include <QResizeEvent>
#include <QStringList>
#include <QToolButton>
#include <QVBoxLayout>
#include <algorithm>
@ -471,19 +473,11 @@ bool PointCloudGLWidget::ensureShader()
"void main() {\n"
" gl_FragColor = vec4(vColor, vAlpha);\n"
"}\n")) {
LOG_ERROR("[CloudView] Shader compile failed: %s\n",
program->log().isEmpty()
? "unknown error"
: qPrintable(program->log()));
delete program;
return false;
}
if (!program->link()) {
LOG_ERROR("[CloudView] Shader link failed: %s\n",
program->log().isEmpty()
? "unknown error"
: qPrintable(program->log()));
delete program;
return false;
}
@ -731,6 +725,7 @@ void PointCloudGLWidget::paintGL()
// 使用 QPainter 绘制坐标轴 XYZ 标注
drawAxisLabels();
drawSelectionInfoOverlay();
}
void PointCloudGLWidget::addPointCloud(const PointCloudXYZ& cloud, const QString& name)
@ -823,7 +818,8 @@ void PointCloudGLWidget::addPointCloud(const PointCloudXYZ& cloud, const QString
update();
}
void PointCloudGLWidget::addPointCloud(const PointCloudXYZRGB& cloud, const QString& name)
void PointCloudGLWidget::addPointCloud(const PointCloudXYZRGB& cloud, const QString& name,
bool resetCamera)
{
if (cloud.empty()) {
return;
@ -929,7 +925,17 @@ void PointCloudGLWidget::addPointCloud(const PointCloudXYZRGB& cloud, const QStr
doneCurrent();
computeBoundingBox();
if (resetCamera) {
resetView();
} else {
// A line-scan cloud grows along X over time. Keep the user's view
// orientation/pan, but expand the camera distance when the retained
// cloud no longer fits in the frustum.
const float fittedDistance = fittedCameraDistance();
if (std::isfinite(fittedDistance)) {
m_distance = qMax(m_distance, fittedDistance);
}
}
update();
}
@ -1021,7 +1027,10 @@ void PointCloudGLWidget::appendPointCloud(const PointCloudXYZRGB& cloud,
doneCurrent();
computeBoundingBox();
m_distance = qMax(m_distance, fittedCameraDistance());
const float fittedDistance = fittedCameraDistance();
if (std::isfinite(fittedDistance)) {
m_distance = qMax(m_distance, fittedDistance);
}
update();
}
@ -1050,6 +1059,23 @@ void PointCloudGLWidget::clearPointClouds()
update();
}
void PointCloudGLWidget::removeOldestPointCloud()
{
if (m_pointClouds.empty()) {
return;
}
// deque::pop_front does not relocate the remaining PointCloudData objects.
makeCurrent();
releaseVBO(m_pointClouds.front());
doneCurrent();
m_pointClouds.pop_front();
m_selectedLine = SelectedLineInfo();
computeBoundingBox();
update();
}
void PointCloudGLWidget::setPointCloudColor(PointCloudColor color)
{
m_currentColor = color;
@ -1113,7 +1139,15 @@ void PointCloudGLWidget::updateProjection(const QMatrix4x4& modelView)
constexpr float kMinimumNearPlane = 0.01f;
float nearPlane = kMinimumNearPlane;
float farPlane = 1.0f;
if (maximumDepth > kMinimumNearPlane) {
const float boundsDiagonal = (m_maxBound - m_minBound).length();
const float safeDiagonal = std::isfinite(boundsDiagonal)
? qMax(boundsDiagonal, 1.0f)
: 1.0f;
const float safeCameraDistance = std::isfinite(m_distance)
? qMax(m_distance, 1.0f)
: 1.0f;
if (std::isfinite(minimumDepth) && std::isfinite(maximumDepth) &&
maximumDepth > kMinimumNearPlane) {
// 点云贴近或跨过相机时保持很小的近裁剪距离,避免前侧点被裁掉。
if (minimumDepth > kMinimumNearPlane) {
nearPlane = qMax(kMinimumNearPlane, minimumDepth * 0.5f);
@ -1121,6 +1155,16 @@ void PointCloudGLWidget::updateProjection(const QMatrix4x4& modelView)
// 远裁剪面仅覆盖当前内容,并限制 near/far 比值以保证深度缓冲精度。
farPlane = qMax(nearPlane + 1.0f, maximumDepth * 1.5f);
nearPlane = qMax(nearPlane, farPlane / 100000.0f);
} else {
// The scan range can cross the current camera plane while the view
// is being updated. Keep a conservative frustum instead of letting
// all points fall outside a near/far clip interval.
nearPlane = kMinimumNearPlane;
farPlane = safeCameraDistance + safeDiagonal * 2.0f;
}
if (!std::isfinite(farPlane) || farPlane <= nearPlane) {
farPlane = nearPlane + safeCameraDistance + safeDiagonal * 2.0f;
}
const float aspect = static_cast<float>(width()) /
@ -1923,25 +1967,84 @@ void PointCloudGLWidget::drawAxisLabels()
painter.end();
}
void PointCloudGLWidget::drawSelectionInfoOverlay()
{
QVector<SelectedPointInfo> validPoints;
for (const SelectedPointInfo& point : m_selectedPoints) {
if (point.valid) {
validPoints.append(point);
}
}
if (validPoints.isEmpty()) {
return;
}
QStringList lines;
for (int i = 0; i < validPoints.size() && i < MAX_SELECTED_POINTS; ++i) {
const SelectedPointInfo& point = validPoints[i];
lines.append(QString("P%1 X: %2 mm Y: %3 mm Z: %4 mm")
.arg(i + 1)
.arg(point.x, 0, 'f', 3)
.arg(point.y, 0, 'f', 3)
.arg(point.z, 0, 'f', 3));
}
if (validPoints.size() >= 2) {
const SelectedPointInfo& p1 = validPoints[0];
const SelectedPointInfo& p2 = validPoints[1];
const float dx = p2.x - p1.x;
const float dy = p2.y - p1.y;
const float dz = p2.z - p1.z;
const float distance = calculateDistance(p1, p2);
const int precision = distance < 0.001f ? 6 : (distance < 1.0f ? 4 : 3);
lines.append(QString("Distance: %1 mm").arg(distance, 0, 'f', precision));
lines.append(QString("dX: %1 mm dY: %2 mm dZ: %3 mm")
.arg(dx, 0, 'f', precision)
.arg(dy, 0, 'f', precision)
.arg(dz, 0, 'f', precision));
}
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.setRenderHint(QPainter::TextAntialiasing);
QFont font = painter.font();
font.setPointSize(10);
painter.setFont(font);
const QFontMetrics metrics(font);
int textWidth = 0;
for (const QString& line : lines) {
textWidth = qMax(textWidth, metrics.horizontalAdvance(line));
}
const int margin = 10;
const int lineHeight = metrics.lineSpacing();
const int textX = qMax(margin, width() - textWidth - margin);
int textY = margin + metrics.ascent();
for (int i = 0; i < lines.size(); ++i) {
painter.setPen(QColor(240, 244, 248));
painter.drawText(textX, textY, lines[i]);
textY += lineHeight;
}
}
void PointCloudGLWidget::mousePressEvent(QMouseEvent* event)
{
m_lastMousePos = event->pos();
if (event->button() == Qt::LeftButton) {
// Ctrl+左键:选点
if (event->modifiers() & Qt::ControlModifier) {
// Alt+左键:依次选择两个点并测距
if (event->modifiers() & Qt::AltModifier) {
makeCurrent();
SelectedPointInfo point = pickPoint(event->pos().x(), event->pos().y());
doneCurrent();
if (point.valid) {
if (m_measureDistanceEnabled) {
// 启用测距:最多保留两个点
if (m_selectedPoints.size() >= MAX_SELECTED_POINTS) {
m_selectedPoints.clear();
}
m_selectedPoints.append(point);
emit pointSelected(point);
if (m_selectedPoints.size() == 2) {
@ -1952,13 +2055,19 @@ void PointCloudGLWidget::mousePressEvent(QMouseEvent* event)
emit twoPointsSelected(m_selectedPoints[0], m_selectedPoints[1], distance);
emit measurementFinished(m_selectedPoints[0], m_selectedPoints[1], distance, dx, dy, dz);
}
} else {
// 未启用测距:只保留一个点
update();
}
} else if (event->modifiers() & Qt::ControlModifier) {
// Ctrl+左键:选择一个点
makeCurrent();
SelectedPointInfo point = pickPoint(event->pos().x(), event->pos().y());
doneCurrent();
if (point.valid) {
m_selectedPoints.clear();
m_selectedPoints.append(point);
emit pointSelected(point);
}
update();
}
} else if (event->modifiers() & Qt::ShiftModifier) {
@ -2058,15 +2167,7 @@ void PointCloudGLWidget::mouseMoveEvent(QMouseEvent* event)
m_lastMousePos = event->pos();
if (m_leftButtonPressed) {
// Alt+左键拖动:绕视线方向旋转(滚转)
if (event->modifiers() & Qt::AltModifier) {
// 绕Z轴(视线方向)旋转(修正方向)
float angle = -delta.x() * 0.5f; // 取反修正方向
QQuaternion deltaRotation = QQuaternion::fromAxisAndAngle(QVector3D(0, 0, 1), angle);
m_rotation = deltaRotation * m_rotation;
m_rotationZ = NormalizeAngleDegrees(m_rotationZ + angle);
} else {
// 普通左键拖动:基于当前物体坐标系的旋转
// 左键拖动:基于当前物体坐标系的旋转
// 鼠标水平移动 -> 绕当前Y轴旋转
// 鼠标垂直移动 -> 绕当前X轴旋转
@ -2086,7 +2187,6 @@ void PointCloudGLWidget::mouseMoveEvent(QMouseEvent* event)
// 更新欧拉角(用于显示),归一化到 ±180°
m_rotationX = NormalizeAngleDegrees(m_rotationX + angleX);
m_rotationY = NormalizeAngleDegrees(m_rotationY + angleY);
}
// 发送角度变化信号
m_distance = qMax(m_distance, minimumCameraDistance());
@ -2130,7 +2230,11 @@ void PointCloudGLWidget::wheelEvent(QWheelEvent* event)
void PointCloudGLWidget::keyPressEvent(QKeyEvent* event)
{
if (event->key() == Qt::Key_Space) {
if (event->key() == Qt::Key_Escape) {
m_selectedPoints.clear();
update();
event->accept();
} else if (event->key() == Qt::Key_Space) {
resetView();
} else {
QOpenGLWidget::keyPressEvent(event);

View File

@ -1,34 +1,112 @@
ADD_DEFINITIONS(-D__OPENVMS__) #log4cpp 编译选项
if(NOT WIN32)
ADD_DEFINITIONS(-DLOG4CPP_HAVE_GETTIMEOFDAY) #ARM/Linux 上直接启用 gettimeofday,确保毫秒正确
endif()
ADD_DEFINITIONS(-DVR_UTILS_EXPORTS) #VrUtils 导出宏定义
# ADD_COMPILE_OPTIONS(-Wno-deprecated-declarations) #auto_ptr警告消除
cmake_minimum_required(VERSION 3.14)
# 设置CMake策略,允许AUTOMOC和AUTOUIC处理.hh文件
# cmake_policy(SET CMP0100 NEW)
INCLUDE_DIRECTORIES(
./_Inc
./Inc
./MD5
./tinyxml2
./log4cpp/include
# Keep this source list aligned with VrUtils.pro. Do not glob Src/: it contains
# legacy standalone sources such as getopt.c that are not part of VrUtils.
set(VRUTILS_SOURCES
Src/VrDateUtils.cpp
Src/VrFileUtils.cpp
Src/VrLog.cpp
Src/VrNetUtils.cpp
Src/VrStringUtils.cpp
Src/VrTimeUtils.cpp
ini/ConvertUTF.c
log4cpp/src/AbortAppender.cpp
log4cpp/src/Appender.cpp
log4cpp/src/AppenderSkeleton.cpp
log4cpp/src/AppendersFactory.cpp
log4cpp/src/BasicConfigurator.cpp
log4cpp/src/BasicLayout.cpp
log4cpp/src/BufferingAppender.cpp
log4cpp/src/Category.cpp
log4cpp/src/CategoryStream.cpp
log4cpp/src/Configurator.cpp
log4cpp/src/DailyRollingFileAppender.cpp
log4cpp/src/DllMain.cpp
log4cpp/src/DummyThreads.cpp
log4cpp/src/FactoryParams.cpp
log4cpp/src/FileAppender.cpp
log4cpp/src/Filter.cpp
log4cpp/src/FixedContextCategory.cpp
log4cpp/src/HierarchyMaintainer.cpp
log4cpp/src/IdsaAppender.cpp
log4cpp/src/LayoutAppender.cpp
log4cpp/src/LayoutsFactory.cpp
log4cpp/src/LevelEvaluator.cpp
log4cpp/src/Localtime.cpp
log4cpp/src/LoggingEvent.cpp
log4cpp/src/MSThreads.cpp
log4cpp/src/Manipulator.cpp
log4cpp/src/NDC.cpp
log4cpp/src/NTEventLogAppender.cpp
log4cpp/src/OmniThreads.cpp
log4cpp/src/OstreamAppender.cpp
log4cpp/src/PThreads.cpp
log4cpp/src/PassThroughLayout.cpp
log4cpp/src/PatternLayout.cpp
log4cpp/src/PortabilityImpl.cpp
log4cpp/src/Priority.cpp
log4cpp/src/Properties.cpp
log4cpp/src/PropertyConfigurator.cpp
log4cpp/src/PropertyConfiguratorImpl.cpp
log4cpp/src/RemoteSyslogAppender.cpp
log4cpp/src/RollingFileAppender.cpp
log4cpp/src/SimpleConfigurator.cpp
log4cpp/src/SimpleLayout.cpp
log4cpp/src/SmtpAppender.cpp
log4cpp/src/StringQueueAppender.cpp
log4cpp/src/StringUtil.cpp
log4cpp/src/SyslogAppender.cpp
log4cpp/src/TimeStamp.cpp
log4cpp/src/TriggeringEventEvaluatorFactory.cpp
log4cpp/src/Win32DebugAppender.cpp
log4cpp/src/snprintf.c
tinyxml2/tinyxml2.cpp
jsoncpp/json_reader.cpp
jsoncpp/json_value.cpp
jsoncpp/json_writer.cpp
)
AUX_SOURCE_DIRECTORY(./Src SrcS)
AUX_SOURCE_DIRECTORY(./MD5 MD5SrcS)
AUX_SOURCE_DIRECTORY(./tinyxml2 TinyxmlSrcS)
AUX_SOURCE_DIRECTORY(./log4cpp/src LogSrcS)
add_library(VrUtils STATIC ${VRUTILS_SOURCES})
# 对特定的.hh文件设置SKIP_AUTOGEN属性
set_property(SOURCE log4cpp/src/StringUtil.hh PROPERTY SKIP_AUTOGEN ON)
set_property(SOURCE log4cpp/src/PortabilityImpl.hh PROPERTY SKIP_AUTOGEN ON)
set_property(SOURCE log4cpp/src/Properties.hh PROPERTY SKIP_AUTOGEN ON)
set_property(SOURCE log4cpp/src/Localtime.hh PROPERTY SKIP_AUTOGEN ON)
set_property(SOURCE log4cpp/src/PropertyConfiguratorImpl.hh PROPERTY SKIP_AUTOGEN ON)
set_target_properties(VrUtils PROPERTIES
CXX_STANDARD 11
CXX_STANDARD_REQUIRED ON
)
ADD_LIBRARY(VrUtils STATIC ${SrcS} ${MD5SrcS}
${TinyxmlSrcS}
${LogSrcS}
)
target_include_directories(VrUtils
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/Inc
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/ini
${CMAKE_CURRENT_SOURCE_DIR}/tinyxml2
${CMAKE_CURRENT_SOURCE_DIR}/jsoncpp
${CMAKE_CURRENT_SOURCE_DIR}/log4cpp/include
${CMAKE_CURRENT_SOURCE_DIR}/log4cpp/src
)
target_compile_definitions(VrUtils PRIVATE
TINYXML2_EXPORT
HAVE_SNPRINTF
LOG4CPP_HAVE_SSTREAM=1
VR_UTILS_EXPORTS
)
if(UNIX AND NOT APPLE)
target_compile_definitions(VrUtils PRIVATE LOG4CPP_HAVE_UNISTD_H)
set_target_properties(VrUtils PROPERTIES POSITION_INDEPENDENT_CODE ON)
endif()
find_package(Threads REQUIRED)
target_link_libraries(VrUtils PUBLIC Threads::Threads)
if(WIN32)
# qmake's Windows mkspec supplies these definitions. log4cpp uses the
# generic Win32 API names and therefore must see the same character mode.
target_compile_definitions(VrUtils PRIVATE UNICODE _UNICODE)
target_link_libraries(VrUtils PUBLIC ws2_32 iphlpapi advapi32 shell32)
endif()
if(MSVC)
target_compile_definitions(VrUtils PRIVATE _HAS_AUTO_PTR_ETC=1)
target_compile_options(VrUtils PRIVATE /utf-8)
endif()