Compare commits

...

7 Commits

Author SHA1 Message Date
9287c8050a merge:合并提取的PathUtils,加图片显示的优化 2026-10-10 23:14:18 +08:00
7b84fffd2a style:调整文件结束符 2026-10-10 19:02:17 +08:00
杰仔
2ee6bcd414 refactor(CloudView): 拆分缩放按钮布局逻辑为独立函数
1.  将原eventFilter中的缩放按钮位置计算代码抽离为updateZoomButtonGeometry独立函数
2.  在创建视图区域时主动调用该函数初始化按钮位置
3.  统一处理GL控件resize时的按钮重定位逻辑,提升代码可读性
4.  修复原代码未在初始化时设置按钮位置的问题
5.  新增按钮置顶操作确保缩放按钮始终显示在最上层
2026-09-11 15:18:33 +08:00
杰仔
72db40115f refactor: 重构项目结构并新增点云查看应用
- 将原CloudView重命名为CloudViewApp
- 新增ImageView可缩放图片控件库
- 新增点云过滤、XYZ范围过滤工具类
- 重构Qt项目配置,移除硬编码输出路径
- 新增版本日志文档和应用图标资源
- 扩展激光数据加载器支持回调流式加载和范围过滤
- 新增点云转换器支持多格式加载和回调加载
- 新增点云查看器配置管理功能
- 统一错误码编号偏移量避免冲突
- 修复CloudView3D项目的代码合并冲突
2026-09-11 15:18:09 +08:00
杰仔
9271a37a81 refactor: 重构项目构建系统与点云查看器功能
1.  新增CloudUtils静态库与相关CMake配置
2.  重构VrUtils构建脚本,修正编译选项与依赖
3.  为Windows平台添加utf-8编译选项
4.  扩展PointCloudGLWidget功能:新增点云移除、选择信息叠加、增量相机适配
5.  优化点云添加接口,支持不重置相机视图
6.  修正激光数据保存时的DataType输出
7.  为Point3D结构体添加单位注释
8.  调整鼠标交互逻辑,统一按键操作行为
2026-08-18 12:26:28 +08:00
杰仔
44180b7f85 refactor(cloudview): 重构项目结构与代码格式,优化构建配置
1.  调整.gitignore规则,新增资源文件忽略项
2.  重构子项目依赖配置,统一项目构建路径
3.  修复CloudView头文件和pro文件的包含路径
4.  格式化代码缩进与注释风格,调整类成员布局
5.  为CloudView新增--help/-h和--version/-v命令行参数
6.  重构点云渲染模块,新增GLES2 shader渲染支持与悬浮控件
2026-08-18 12:26:08 +08:00
杰仔
74072f9620 增加3D数据的移除功能 2026-08-12 14:48:18 +08:00
22 changed files with 4482 additions and 3527 deletions

23
CMakeLists.txt Normal file
View File

@ -0,0 +1,23 @@
cmake_minimum_required(VERSION 3.14)
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.
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()

55
CloudUtils/CMakeLists.txt Normal file
View File

@ -0,0 +1,55 @@
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
)
# When CamTool is built with WoodSDK, use its VZ compatibility types first.
# This keeps LaserDataLoader's SVzNL* signatures identical to the application
# translation units, where WoodSDK maps them to the public SWD* structures.
if(TARGET WoodSDK)
target_include_directories(CloudUtils BEFORE PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/../../App/woodcam-sdk/WoodSDK/VzToWDInc"
)
endif()
target_link_libraries(CloudUtils
PUBLIC
VrCommon
Qt5::Core
Qt5::Gui
PRIVATE
VrUtils
)
if(MSVC)
target_compile_options(CloudUtils PRIVATE /utf-8)
endif()

View File

@ -1,37 +1,37 @@
# CloudUtils搴撻」鐩厤缃枃浠?
# 鎸囧畾Qt妯″潡
QT += core
# 鎸囧畾椤圭洰妯℃澘涓洪潤鎬佸簱
TEMPLATE = lib
CONFIG += staticlib
win32-msvc {
QMAKE_CXXFLAGS += /utf-8
}
# 鎸囧畾C++鏍囧噯
CONFIG += c++11
INCLUDEPATH += $$PWD/../../Device/SDK/VzNLSDK/_Inc
INCLUDEPATH += $$PWD/../../Device/SDK/VzNLSDK/Inc
INCLUDEPATH += $$PWD/../VrCommon/Inc
INCLUDEPATH += $$PWD/../VrUtils/Inc
# 鎸囧畾鍖呭惈璺緞
INCLUDEPATH += ./Inc \
./Src
# 鎸囧畾澶存枃浠?
HEADERS += Inc/LaserDataLoader.h \
Inc/PointCloudImageUtils.h \
Inc/PointCloudFilter.h \
Inc/PointCloudXYZRange.h
# 鎸囧畾婧愭枃浠?
SOURCES += Src/LaserDataLoader.cpp \
Src/PointCloudImageUtils.cpp \
Src/PointCloudFilter.cpp
# CloudUtils搴撻」鐩厤缃枃浠?
# 鎸囧畾Qt妯″潡
QT += core
# 鎸囧畾椤圭洰妯℃澘涓洪潤鎬佸簱
TEMPLATE = lib
CONFIG += staticlib
win32-msvc {
QMAKE_CXXFLAGS += /utf-8
}
# 鎸囧畾C++鏍囧噯
CONFIG += c++11
INCLUDEPATH += $$PWD/../../Device/SDK/VzNLSDK/_Inc
INCLUDEPATH += $$PWD/../../Device/SDK/VzNLSDK/Inc
INCLUDEPATH += $$PWD/../VrCommon/Inc
INCLUDEPATH += $$PWD/../VrUtils/Inc
# 鎸囧畾鍖呭惈璺緞
INCLUDEPATH += ./Inc \
./Src
# 鎸囧畾澶存枃浠?
HEADERS += Inc/LaserDataLoader.h \
Inc/PointCloudImageUtils.h \
Inc/PointCloudFilter.h \
Inc/PointCloudXYZRange.h
# 鎸囧畾婧愭枃浠?
SOURCES += Src/LaserDataLoader.cpp \
Src/PointCloudImageUtils.cpp \
Src/PointCloudFilter.cpp

View File

@ -1,59 +1,59 @@
#ifndef POINTCLOUDFILTER_H
#define POINTCLOUDFILTER_H
#include <cstddef>
#include <vector>
#include "VZNL_Types.h"
// Selects the outlier removal algorithm.
enum class PointCloudFilterMethod
{
None = 0,
ScanLineContinuity,
RadiusOutlier,
StatisticalOutlier
};
struct PointCloudFilterParams
{
PointCloudFilterMethod method = PointCloudFilterMethod::None;
// ScanLineContinuity parameters. Units follow the point cloud (normally mm).
double continuityThreshold = 20.0;
std::size_t minSegmentLength = 5;
// RadiusOutlier parameters. minNeighbors excludes the point itself.
double radius = 5.0;
std::size_t minNeighbors = 2;
// StatisticalOutlier parameters.
std::size_t meanK = 8;
double standardDeviationMultiplier = 1.0;
// Zero estimates a radius from the cloud density; non-zero limits the search.
double searchRadius = 0.0;
};
struct PointCloudFilterStats
{
std::size_t inputPointCount = 0;
std::size_t outputPointCount = 0;
std::size_t removedPointCount = 0;
};
class PointCloudFilter
{
public:
// The input is not modified. Original nPointIdx values are retained.
static bool Filter(const std::vector<std::vector<SVzNL3DPosition>>& input,
std::vector<std::vector<SVzNL3DPosition>>& output,
const PointCloudFilterParams& params = PointCloudFilterParams(),
PointCloudFilterStats* stats = nullptr);
static bool Filter(const std::vector<SVzNL3DPosition>& input,
std::vector<SVzNL3DPosition>& output,
const PointCloudFilterParams& params = PointCloudFilterParams(),
PointCloudFilterStats* stats = nullptr);
};
#endif // POINTCLOUDFILTER_H
#ifndef POINTCLOUDFILTER_H
#define POINTCLOUDFILTER_H
#include <cstddef>
#include <vector>
#include "VZNL_Types.h"
// Selects the outlier removal algorithm.
enum class PointCloudFilterMethod
{
None = 0,
ScanLineContinuity,
RadiusOutlier,
StatisticalOutlier
};
struct PointCloudFilterParams
{
PointCloudFilterMethod method = PointCloudFilterMethod::None;
// ScanLineContinuity parameters. Units follow the point cloud (normally mm).
double continuityThreshold = 20.0;
std::size_t minSegmentLength = 5;
// RadiusOutlier parameters. minNeighbors excludes the point itself.
double radius = 5.0;
std::size_t minNeighbors = 2;
// StatisticalOutlier parameters.
std::size_t meanK = 8;
double standardDeviationMultiplier = 1.0;
// Zero estimates a radius from the cloud density; non-zero limits the search.
double searchRadius = 0.0;
};
struct PointCloudFilterStats
{
std::size_t inputPointCount = 0;
std::size_t outputPointCount = 0;
std::size_t removedPointCount = 0;
};
class PointCloudFilter
{
public:
// The input is not modified. Original nPointIdx values are retained.
static bool Filter(const std::vector<std::vector<SVzNL3DPosition>>& input,
std::vector<std::vector<SVzNL3DPosition>>& output,
const PointCloudFilterParams& params = PointCloudFilterParams(),
PointCloudFilterStats* stats = nullptr);
static bool Filter(const std::vector<SVzNL3DPosition>& input,
std::vector<SVzNL3DPosition>& output,
const PointCloudFilterParams& params = PointCloudFilterParams(),
PointCloudFilterStats* stats = nullptr);
};
#endif // POINTCLOUDFILTER_H

View File

@ -1,72 +1,72 @@
#ifndef POINT_CLOUD_XYZ_RANGE_H
#define POINT_CLOUD_XYZ_RANGE_H
#include <cmath>
#include <limits>
// XYZ 范围外点的处理方式。
//
// ReplaceWithZero 保留点在线内的原始槽位,并仅把三维坐标置为零;
// Remove 则删除范围外点并压缩扫描线。保留槽位对于按扫描线/网格
// 处理的调用方很重要,因此它也是启用范围过滤时的默认模式。
enum class PointCloudXYZRangeFilterMode
{
ReplaceWithZero = 0,
Remove = 1
};
// 兼容不需要区分 XYZ 范围限定名称的调用方。
using PointCloudRangeFilterMode = PointCloudXYZRangeFilterMode;
// 点云加载使用的可选轴对齐范围;默认关闭以保持原有读取行为。
struct PointCloudXYZRange
{
using FilterMode = PointCloudXYZRangeFilterMode;
bool enabled = false;
double minX = -std::numeric_limits<double>::infinity();
double maxX = std::numeric_limits<double>::infinity();
double minY = -std::numeric_limits<double>::infinity();
double maxY = std::numeric_limits<double>::infinity();
double minZ = -std::numeric_limits<double>::infinity();
double maxZ = std::numeric_limits<double>::infinity();
// Keep this after the legacy bounds so aggregate initialization remains compatible.
PointCloudXYZRangeFilterMode filterMode =
PointCloudXYZRangeFilterMode::ReplaceWithZero;
bool isValid() const
{
if (!enabled)
{
return true;
}
return !std::isnan(minX) && !std::isnan(maxX)
&& !std::isnan(minY) && !std::isnan(maxY)
&& !std::isnan(minZ) && !std::isnan(maxZ)
&& minX <= maxX && minY <= maxY && minZ <= maxZ;
}
bool contains(double x, double y, double z) const
{
if (!enabled)
{
return true;
}
if (!isValid() || !std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z))
{
return false;
}
return x >= minX && x <= maxX
&& y >= minY && y <= maxY
&& z >= minZ && z <= maxZ;
}
bool isEnabled() const
{
return enabled && isValid();
}
};
using PointCloudRange = PointCloudXYZRange;
#endif // POINT_CLOUD_XYZ_RANGE_H
#ifndef POINT_CLOUD_XYZ_RANGE_H
#define POINT_CLOUD_XYZ_RANGE_H
#include <cmath>
#include <limits>
// XYZ 范围外点的处理方式。
//
// ReplaceWithZero 保留点在线内的原始槽位,并仅把三维坐标置为零;
// Remove 则删除范围外点并压缩扫描线。保留槽位对于按扫描线/网格
// 处理的调用方很重要,因此它也是启用范围过滤时的默认模式。
enum class PointCloudXYZRangeFilterMode
{
ReplaceWithZero = 0,
Remove = 1
};
// 兼容不需要区分 XYZ 范围限定名称的调用方。
using PointCloudRangeFilterMode = PointCloudXYZRangeFilterMode;
// 点云加载使用的可选轴对齐范围;默认关闭以保持原有读取行为。
struct PointCloudXYZRange
{
using FilterMode = PointCloudXYZRangeFilterMode;
bool enabled = false;
double minX = -std::numeric_limits<double>::infinity();
double maxX = std::numeric_limits<double>::infinity();
double minY = -std::numeric_limits<double>::infinity();
double maxY = std::numeric_limits<double>::infinity();
double minZ = -std::numeric_limits<double>::infinity();
double maxZ = std::numeric_limits<double>::infinity();
// Keep this after the legacy bounds so aggregate initialization remains compatible.
PointCloudXYZRangeFilterMode filterMode =
PointCloudXYZRangeFilterMode::ReplaceWithZero;
bool isValid() const
{
if (!enabled)
{
return true;
}
return !std::isnan(minX) && !std::isnan(maxX)
&& !std::isnan(minY) && !std::isnan(maxY)
&& !std::isnan(minZ) && !std::isnan(maxZ)
&& minX <= maxX && minY <= maxY && minZ <= maxZ;
}
bool contains(double x, double y, double z) const
{
if (!enabled)
{
return true;
}
if (!isValid() || !std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z))
{
return false;
}
return x >= minX && x <= maxX
&& y >= minY && y <= maxY
&& z >= minZ && z <= maxZ;
}
bool isEnabled() const
{
return enabled && isValid();
}
};
using PointCloudRange = PointCloudXYZRange;
#endif // POINT_CLOUD_XYZ_RANGE_H

File diff suppressed because it is too large Load Diff

View File

@ -1,383 +1,383 @@
#include "PointCloudFilter.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include <numeric>
#include <unordered_map>
namespace
{
struct GridKey
{
long long x;
long long y;
long long z;
bool operator==(const GridKey& other) const
{
return x == other.x && y == other.y && z == other.z;
}
};
struct GridKeyHash
{
std::size_t operator()(const GridKey& key) const
{
const std::size_t h1 = std::hash<long long>()(key.x);
const std::size_t h2 = std::hash<long long>()(key.y);
const std::size_t h3 = std::hash<long long>()(key.z);
return h1 ^ (h2 + static_cast<std::size_t>(0x9e3779b9) + (h1 << 6) + (h1 >> 2))
^ (h3 + static_cast<std::size_t>(0x9e3779b9) + (h2 << 6) + (h2 >> 2));
}
};
struct FlatPoint
{
SVzNL3DPosition point;
};
bool IsFinite(const SVzNL3DPosition& point)
{
return std::isfinite(point.pt3D.x) && std::isfinite(point.pt3D.y)
&& std::isfinite(point.pt3D.z);
}
double SquaredDistance(const SVzNL3DPosition& first, const SVzNL3DPosition& second)
{
const double dx = first.pt3D.x - second.pt3D.x;
const double dy = first.pt3D.y - second.pt3D.y;
const double dz = first.pt3D.z - second.pt3D.z;
return dx * dx + dy * dy + dz * dz;
}
long long ToCell(double value, double cellSize)
{
const double coordinate = std::floor(value / cellSize);
if (coordinate <= static_cast<double>(std::numeric_limits<long long>::min())) {
return std::numeric_limits<long long>::min();
}
if (coordinate >= static_cast<double>(std::numeric_limits<long long>::max())) {
return std::numeric_limits<long long>::max();
}
return static_cast<long long>(coordinate);
}
GridKey GetKey(const SVzNL3DPosition& point, double cellSize)
{
GridKey key;
key.x = ToCell(point.pt3D.x, cellSize);
key.y = ToCell(point.pt3D.y, cellSize);
key.z = ToCell(point.pt3D.z, cellSize);
return key;
}
void ResetStats(PointCloudFilterStats* stats, std::size_t inputCount)
{
if (stats != nullptr) {
stats->inputPointCount = inputCount;
stats->outputPointCount = 0;
stats->removedPointCount = 0;
}
}
void FinishStats(PointCloudFilterStats* stats, std::size_t outputCount)
{
if (stats != nullptr) {
stats->outputPointCount = outputCount;
stats->removedPointCount = stats->inputPointCount >= outputCount
? stats->inputPointCount - outputCount : 0;
}
}
bool FilterScanLine(const std::vector<SVzNL3DPosition>& input,
std::vector<SVzNL3DPosition>& output,
const PointCloudFilterParams& params)
{
output.clear();
if (input.empty()) {
return true;
}
const std::size_t minLength = std::max<std::size_t>(1, params.minSegmentLength);
std::size_t segmentBegin = 0;
for (std::size_t index = 1; index <= input.size(); ++index) {
bool connected = index < input.size() && IsFinite(input[index - 1])
&& IsFinite(input[index])
&& std::abs(input[index].pt3D.z - input[index - 1].pt3D.z)
<= std::max(0.0, params.continuityThreshold);
if (index == input.size() || !connected) {
if (index - segmentBegin >= minLength) {
output.insert(output.end(), input.begin() + static_cast<std::ptrdiff_t>(segmentBegin),
input.begin() + static_cast<std::ptrdiff_t>(index));
}
segmentBegin = index;
}
}
return true;
}
double EstimateCellSize(const std::vector<FlatPoint>& points)
{
if (points.size() < 2) {
return 1.0;
}
double minX = points[0].point.pt3D.x;
double maxX = minX;
double minY = points[0].point.pt3D.y;
double maxY = minY;
double minZ = points[0].point.pt3D.z;
double maxZ = minZ;
for (std::size_t index = 1; index < points.size(); ++index) {
const SVzNL3DPoint& value = points[index].point.pt3D;
minX = std::min(minX, value.x);
maxX = std::max(maxX, value.x);
minY = std::min(minY, value.y);
maxY = std::max(maxY, value.y);
minZ = std::min(minZ, value.z);
maxZ = std::max(maxZ, value.z);
}
const double diagonal = std::sqrt((maxX - minX) * (maxX - minX)
+ (maxY - minY) * (maxY - minY) + (maxZ - minZ) * (maxZ - minZ));
const double scale = diagonal / std::cbrt(static_cast<double>(points.size()));
return std::max(scale, 1e-6);
}
template <typename KeepPredicate>
void FilterWithGrid(const std::vector<FlatPoint>& points, double cellSize,
double radius, KeepPredicate keepPoint,
std::vector<bool>& keep)
{
std::unordered_map<GridKey, std::vector<std::size_t>, GridKeyHash> grid;
grid.reserve(points.size() * 2 + 1);
for (std::size_t index = 0; index < points.size(); ++index) {
grid[GetKey(points[index].point, cellSize)].push_back(index);
}
const long long cellRange = std::max<long long>(1, static_cast<long long>(std::ceil(radius / cellSize)));
const double radiusSquared = radius * radius;
std::vector<std::size_t> candidates;
candidates.reserve(32);
for (std::size_t index = 0; index < points.size(); ++index) {
const GridKey center = GetKey(points[index].point, cellSize);
candidates.clear();
for (long long x = -cellRange; x <= cellRange; ++x) {
for (long long y = -cellRange; y <= cellRange; ++y) {
for (long long z = -cellRange; z <= cellRange; ++z) {
GridKey key = {center.x + x, center.y + y, center.z + z};
const auto found = grid.find(key);
if (found != grid.end()) {
candidates.insert(candidates.end(), found->second.begin(), found->second.end());
}
}
}
}
keep[index] = keepPoint(index, candidates, radiusSquared);
}
}
}
bool PointCloudFilter::Filter(const std::vector<std::vector<SVzNL3DPosition>>& input,
std::vector<std::vector<SVzNL3DPosition>>& output,
const PointCloudFilterParams& params,
PointCloudFilterStats* stats)
{
std::size_t inputCount = 0;
for (const auto& line : input) {
inputCount += line.size();
}
ResetStats(stats, inputCount);
output.clear();
output.resize(input.size());
if (params.method == PointCloudFilterMethod::None) {
output = input;
FinishStats(stats, inputCount);
return true;
}
if (params.method == PointCloudFilterMethod::ScanLineContinuity) {
std::size_t outputCount = 0;
for (std::size_t lineIndex = 0; lineIndex < input.size(); ++lineIndex) {
FilterScanLine(input[lineIndex], output[lineIndex], params);
outputCount += output[lineIndex].size();
}
FinishStats(stats, outputCount);
return true;
}
if (params.method != PointCloudFilterMethod::RadiusOutlier
&& params.method != PointCloudFilterMethod::StatisticalOutlier) {
output.clear();
return false;
}
if (params.method == PointCloudFilterMethod::RadiusOutlier && params.radius <= 0.0) {
output.clear();
return false;
}
std::vector<FlatPoint> points;
points.reserve(inputCount);
for (const auto& line : input) {
for (const SVzNL3DPosition& point : line) {
if (IsFinite(point)) {
points.push_back({point});
}
}
}
std::vector<SVzNL3DPosition> filtered;
std::vector<SVzNL3DPosition> flatInput;
flatInput.reserve(points.size());
for (std::size_t index = 0; index < points.size(); ++index) {
flatInput.push_back(points[index].point);
// Encode the flattened position only for reconstructing scan lines.
flatInput.back().nPointIdx = static_cast<int>(index);
}
if (!Filter(flatInput, filtered, params, nullptr)) {
output.clear();
return false;
}
std::vector<bool> retained(points.size(), false);
for (const SVzNL3DPosition& point : filtered) {
if (point.nPointIdx >= 0
&& static_cast<std::size_t>(point.nPointIdx) < retained.size()) {
retained[static_cast<std::size_t>(point.nPointIdx)] = true;
}
}
std::size_t pointIndex = 0;
for (std::size_t lineIndex = 0; lineIndex < input.size(); ++lineIndex) {
for (const SVzNL3DPosition& point : input[lineIndex]) {
if (IsFinite(point)) {
if (pointIndex < retained.size() && retained[pointIndex]) {
output[lineIndex].push_back(point);
}
++pointIndex;
}
}
}
std::size_t outputCount = 0;
for (const auto& line : output) {
outputCount += line.size();
}
FinishStats(stats, outputCount);
return true;
}
bool PointCloudFilter::Filter(const std::vector<SVzNL3DPosition>& input,
std::vector<SVzNL3DPosition>& output,
const PointCloudFilterParams& params,
PointCloudFilterStats* stats)
{
ResetStats(stats, input.size());
output.clear();
if (params.method == PointCloudFilterMethod::None) {
output = input;
FinishStats(stats, output.size());
return true;
}
if (params.method == PointCloudFilterMethod::ScanLineContinuity) {
FilterScanLine(input, output, params);
FinishStats(stats, output.size());
return true;
}
if (params.method != PointCloudFilterMethod::RadiusOutlier
&& params.method != PointCloudFilterMethod::StatisticalOutlier) {
return false;
}
if (params.method == PointCloudFilterMethod::RadiusOutlier && params.radius <= 0.0) {
return false;
}
std::vector<FlatPoint> points;
points.reserve(input.size());
for (const SVzNL3DPosition& point : input) {
if (IsFinite(point)) {
points.push_back({point});
}
}
if (points.empty()) {
FinishStats(stats, 0);
return true;
}
const double requestedRadius = params.method == PointCloudFilterMethod::RadiusOutlier
? params.radius : params.searchRadius;
const double radius = requestedRadius > 0.0 ? requestedRadius : EstimateCellSize(points) * 2.0;
const double cellSize = std::max(radius, 1e-6);
std::vector<bool> keep(points.size(), false);
if (params.method == PointCloudFilterMethod::RadiusOutlier) {
const std::size_t minNeighbors = params.minNeighbors;
FilterWithGrid(points, cellSize, radius,
[&points, minNeighbors](std::size_t index, const std::vector<std::size_t>& candidates,
double radiusSquared) {
std::size_t neighbors = 0;
for (std::size_t candidate : candidates) {
if (candidate != index && SquaredDistance(points[index].point, points[candidate].point) <= radiusSquared) {
++neighbors;
}
}
return neighbors >= minNeighbors;
}, keep);
}
if (params.method == PointCloudFilterMethod::StatisticalOutlier) {
std::vector<double> meanDistances(points.size(), std::numeric_limits<double>::infinity());
FilterWithGrid(points, cellSize, radius,
[&points, &meanDistances, &params](std::size_t index,
const std::vector<std::size_t>& candidates,
double radiusSquared) {
std::vector<double> distances;
distances.reserve(candidates.size());
for (std::size_t candidate : candidates) {
if (candidate != index) {
const double distance = SquaredDistance(points[index].point, points[candidate].point);
if (distance <= radiusSquared) {
distances.push_back(std::sqrt(distance));
}
}
}
if (distances.empty()) {
return false;
}
const std::size_t count = std::min(params.meanK == 0 ? std::size_t(1) : params.meanK,
distances.size());
if (count < distances.size()) {
std::nth_element(distances.begin(),
distances.begin() + static_cast<std::ptrdiff_t>(count),
distances.end());
}
meanDistances[index] = std::accumulate(distances.begin(), distances.begin() + static_cast<std::ptrdiff_t>(count), 0.0)
/ static_cast<double>(count);
return true;
}, keep);
std::vector<double> validMeans;
for (double value : meanDistances) {
if (std::isfinite(value)) {
validMeans.push_back(value);
}
}
if (!validMeans.empty()) {
const double mean = std::accumulate(validMeans.begin(), validMeans.end(), 0.0)
/ static_cast<double>(validMeans.size());
double variance = 0.0;
for (double value : validMeans) {
const double delta = value - mean;
variance += delta * delta;
}
const double deviation = std::sqrt(variance / static_cast<double>(validMeans.size()));
const double threshold = mean + std::max(0.0, params.standardDeviationMultiplier) * deviation;
for (std::size_t index = 0; index < keep.size(); ++index) {
keep[index] = std::isfinite(meanDistances[index]) && meanDistances[index] <= threshold;
}
}
}
output.reserve(points.size());
for (std::size_t index = 0; index < points.size(); ++index) {
if (keep[index]) {
output.push_back(points[index].point);
}
}
FinishStats(stats, output.size());
return true;
}
#include "PointCloudFilter.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include <numeric>
#include <unordered_map>
namespace
{
struct GridKey
{
long long x;
long long y;
long long z;
bool operator==(const GridKey& other) const
{
return x == other.x && y == other.y && z == other.z;
}
};
struct GridKeyHash
{
std::size_t operator()(const GridKey& key) const
{
const std::size_t h1 = std::hash<long long>()(key.x);
const std::size_t h2 = std::hash<long long>()(key.y);
const std::size_t h3 = std::hash<long long>()(key.z);
return h1 ^ (h2 + static_cast<std::size_t>(0x9e3779b9) + (h1 << 6) + (h1 >> 2))
^ (h3 + static_cast<std::size_t>(0x9e3779b9) + (h2 << 6) + (h2 >> 2));
}
};
struct FlatPoint
{
SVzNL3DPosition point;
};
bool IsFinite(const SVzNL3DPosition& point)
{
return std::isfinite(point.pt3D.x) && std::isfinite(point.pt3D.y)
&& std::isfinite(point.pt3D.z);
}
double SquaredDistance(const SVzNL3DPosition& first, const SVzNL3DPosition& second)
{
const double dx = first.pt3D.x - second.pt3D.x;
const double dy = first.pt3D.y - second.pt3D.y;
const double dz = first.pt3D.z - second.pt3D.z;
return dx * dx + dy * dy + dz * dz;
}
long long ToCell(double value, double cellSize)
{
const double coordinate = std::floor(value / cellSize);
if (coordinate <= static_cast<double>(std::numeric_limits<long long>::min())) {
return std::numeric_limits<long long>::min();
}
if (coordinate >= static_cast<double>(std::numeric_limits<long long>::max())) {
return std::numeric_limits<long long>::max();
}
return static_cast<long long>(coordinate);
}
GridKey GetKey(const SVzNL3DPosition& point, double cellSize)
{
GridKey key;
key.x = ToCell(point.pt3D.x, cellSize);
key.y = ToCell(point.pt3D.y, cellSize);
key.z = ToCell(point.pt3D.z, cellSize);
return key;
}
void ResetStats(PointCloudFilterStats* stats, std::size_t inputCount)
{
if (stats != nullptr) {
stats->inputPointCount = inputCount;
stats->outputPointCount = 0;
stats->removedPointCount = 0;
}
}
void FinishStats(PointCloudFilterStats* stats, std::size_t outputCount)
{
if (stats != nullptr) {
stats->outputPointCount = outputCount;
stats->removedPointCount = stats->inputPointCount >= outputCount
? stats->inputPointCount - outputCount : 0;
}
}
bool FilterScanLine(const std::vector<SVzNL3DPosition>& input,
std::vector<SVzNL3DPosition>& output,
const PointCloudFilterParams& params)
{
output.clear();
if (input.empty()) {
return true;
}
const std::size_t minLength = std::max<std::size_t>(1, params.minSegmentLength);
std::size_t segmentBegin = 0;
for (std::size_t index = 1; index <= input.size(); ++index) {
bool connected = index < input.size() && IsFinite(input[index - 1])
&& IsFinite(input[index])
&& std::abs(input[index].pt3D.z - input[index - 1].pt3D.z)
<= std::max(0.0, params.continuityThreshold);
if (index == input.size() || !connected) {
if (index - segmentBegin >= minLength) {
output.insert(output.end(), input.begin() + static_cast<std::ptrdiff_t>(segmentBegin),
input.begin() + static_cast<std::ptrdiff_t>(index));
}
segmentBegin = index;
}
}
return true;
}
double EstimateCellSize(const std::vector<FlatPoint>& points)
{
if (points.size() < 2) {
return 1.0;
}
double minX = points[0].point.pt3D.x;
double maxX = minX;
double minY = points[0].point.pt3D.y;
double maxY = minY;
double minZ = points[0].point.pt3D.z;
double maxZ = minZ;
for (std::size_t index = 1; index < points.size(); ++index) {
const SVzNL3DPoint& value = points[index].point.pt3D;
minX = std::min(minX, value.x);
maxX = std::max(maxX, value.x);
minY = std::min(minY, value.y);
maxY = std::max(maxY, value.y);
minZ = std::min(minZ, value.z);
maxZ = std::max(maxZ, value.z);
}
const double diagonal = std::sqrt((maxX - minX) * (maxX - minX)
+ (maxY - minY) * (maxY - minY) + (maxZ - minZ) * (maxZ - minZ));
const double scale = diagonal / std::cbrt(static_cast<double>(points.size()));
return std::max(scale, 1e-6);
}
template <typename KeepPredicate>
void FilterWithGrid(const std::vector<FlatPoint>& points, double cellSize,
double radius, KeepPredicate keepPoint,
std::vector<bool>& keep)
{
std::unordered_map<GridKey, std::vector<std::size_t>, GridKeyHash> grid;
grid.reserve(points.size() * 2 + 1);
for (std::size_t index = 0; index < points.size(); ++index) {
grid[GetKey(points[index].point, cellSize)].push_back(index);
}
const long long cellRange = std::max<long long>(1, static_cast<long long>(std::ceil(radius / cellSize)));
const double radiusSquared = radius * radius;
std::vector<std::size_t> candidates;
candidates.reserve(32);
for (std::size_t index = 0; index < points.size(); ++index) {
const GridKey center = GetKey(points[index].point, cellSize);
candidates.clear();
for (long long x = -cellRange; x <= cellRange; ++x) {
for (long long y = -cellRange; y <= cellRange; ++y) {
for (long long z = -cellRange; z <= cellRange; ++z) {
GridKey key = {center.x + x, center.y + y, center.z + z};
const auto found = grid.find(key);
if (found != grid.end()) {
candidates.insert(candidates.end(), found->second.begin(), found->second.end());
}
}
}
}
keep[index] = keepPoint(index, candidates, radiusSquared);
}
}
}
bool PointCloudFilter::Filter(const std::vector<std::vector<SVzNL3DPosition>>& input,
std::vector<std::vector<SVzNL3DPosition>>& output,
const PointCloudFilterParams& params,
PointCloudFilterStats* stats)
{
std::size_t inputCount = 0;
for (const auto& line : input) {
inputCount += line.size();
}
ResetStats(stats, inputCount);
output.clear();
output.resize(input.size());
if (params.method == PointCloudFilterMethod::None) {
output = input;
FinishStats(stats, inputCount);
return true;
}
if (params.method == PointCloudFilterMethod::ScanLineContinuity) {
std::size_t outputCount = 0;
for (std::size_t lineIndex = 0; lineIndex < input.size(); ++lineIndex) {
FilterScanLine(input[lineIndex], output[lineIndex], params);
outputCount += output[lineIndex].size();
}
FinishStats(stats, outputCount);
return true;
}
if (params.method != PointCloudFilterMethod::RadiusOutlier
&& params.method != PointCloudFilterMethod::StatisticalOutlier) {
output.clear();
return false;
}
if (params.method == PointCloudFilterMethod::RadiusOutlier && params.radius <= 0.0) {
output.clear();
return false;
}
std::vector<FlatPoint> points;
points.reserve(inputCount);
for (const auto& line : input) {
for (const SVzNL3DPosition& point : line) {
if (IsFinite(point)) {
points.push_back({point});
}
}
}
std::vector<SVzNL3DPosition> filtered;
std::vector<SVzNL3DPosition> flatInput;
flatInput.reserve(points.size());
for (std::size_t index = 0; index < points.size(); ++index) {
flatInput.push_back(points[index].point);
// Encode the flattened position only for reconstructing scan lines.
flatInput.back().nPointIdx = static_cast<int>(index);
}
if (!Filter(flatInput, filtered, params, nullptr)) {
output.clear();
return false;
}
std::vector<bool> retained(points.size(), false);
for (const SVzNL3DPosition& point : filtered) {
if (point.nPointIdx >= 0
&& static_cast<std::size_t>(point.nPointIdx) < retained.size()) {
retained[static_cast<std::size_t>(point.nPointIdx)] = true;
}
}
std::size_t pointIndex = 0;
for (std::size_t lineIndex = 0; lineIndex < input.size(); ++lineIndex) {
for (const SVzNL3DPosition& point : input[lineIndex]) {
if (IsFinite(point)) {
if (pointIndex < retained.size() && retained[pointIndex]) {
output[lineIndex].push_back(point);
}
++pointIndex;
}
}
}
std::size_t outputCount = 0;
for (const auto& line : output) {
outputCount += line.size();
}
FinishStats(stats, outputCount);
return true;
}
bool PointCloudFilter::Filter(const std::vector<SVzNL3DPosition>& input,
std::vector<SVzNL3DPosition>& output,
const PointCloudFilterParams& params,
PointCloudFilterStats* stats)
{
ResetStats(stats, input.size());
output.clear();
if (params.method == PointCloudFilterMethod::None) {
output = input;
FinishStats(stats, output.size());
return true;
}
if (params.method == PointCloudFilterMethod::ScanLineContinuity) {
FilterScanLine(input, output, params);
FinishStats(stats, output.size());
return true;
}
if (params.method != PointCloudFilterMethod::RadiusOutlier
&& params.method != PointCloudFilterMethod::StatisticalOutlier) {
return false;
}
if (params.method == PointCloudFilterMethod::RadiusOutlier && params.radius <= 0.0) {
return false;
}
std::vector<FlatPoint> points;
points.reserve(input.size());
for (const SVzNL3DPosition& point : input) {
if (IsFinite(point)) {
points.push_back({point});
}
}
if (points.empty()) {
FinishStats(stats, 0);
return true;
}
const double requestedRadius = params.method == PointCloudFilterMethod::RadiusOutlier
? params.radius : params.searchRadius;
const double radius = requestedRadius > 0.0 ? requestedRadius : EstimateCellSize(points) * 2.0;
const double cellSize = std::max(radius, 1e-6);
std::vector<bool> keep(points.size(), false);
if (params.method == PointCloudFilterMethod::RadiusOutlier) {
const std::size_t minNeighbors = params.minNeighbors;
FilterWithGrid(points, cellSize, radius,
[&points, minNeighbors](std::size_t index, const std::vector<std::size_t>& candidates,
double radiusSquared) {
std::size_t neighbors = 0;
for (std::size_t candidate : candidates) {
if (candidate != index && SquaredDistance(points[index].point, points[candidate].point) <= radiusSquared) {
++neighbors;
}
}
return neighbors >= minNeighbors;
}, keep);
}
if (params.method == PointCloudFilterMethod::StatisticalOutlier) {
std::vector<double> meanDistances(points.size(), std::numeric_limits<double>::infinity());
FilterWithGrid(points, cellSize, radius,
[&points, &meanDistances, &params](std::size_t index,
const std::vector<std::size_t>& candidates,
double radiusSquared) {
std::vector<double> distances;
distances.reserve(candidates.size());
for (std::size_t candidate : candidates) {
if (candidate != index) {
const double distance = SquaredDistance(points[index].point, points[candidate].point);
if (distance <= radiusSquared) {
distances.push_back(std::sqrt(distance));
}
}
}
if (distances.empty()) {
return false;
}
const std::size_t count = std::min(params.meanK == 0 ? std::size_t(1) : params.meanK,
distances.size());
if (count < distances.size()) {
std::nth_element(distances.begin(),
distances.begin() + static_cast<std::ptrdiff_t>(count),
distances.end());
}
meanDistances[index] = std::accumulate(distances.begin(), distances.begin() + static_cast<std::ptrdiff_t>(count), 0.0)
/ static_cast<double>(count);
return true;
}, keep);
std::vector<double> validMeans;
for (double value : meanDistances) {
if (std::isfinite(value)) {
validMeans.push_back(value);
}
}
if (!validMeans.empty()) {
const double mean = std::accumulate(validMeans.begin(), validMeans.end(), 0.0)
/ static_cast<double>(validMeans.size());
double variance = 0.0;
for (double value : validMeans) {
const double delta = value - mean;
variance += delta * delta;
}
const double deviation = std::sqrt(variance / static_cast<double>(validMeans.size()));
const double threshold = mean + std::max(0.0, params.standardDeviationMultiplier) * deviation;
for (std::size_t index = 0; index < keep.size(); ++index) {
keep[index] = std::isfinite(meanDistances[index]) && meanDistances[index] <= threshold;
}
}
}
output.reserve(points.size());
for (std::size_t index = 0; index < points.size(); ++index) {
if (keep[index]) {
output.push_back(points[index].point);
}
}
FinishStats(stats, output.size());
return true;
}

View File

@ -0,0 +1,43 @@
cmake_minimum_required(VERSION 3.14)
find_package(Qt5 REQUIRED COMPONENTS Core Gui Widgets OpenGL)
if(NOT TARGET VrCommon)
add_library(VrCommon INTERFACE)
target_include_directories(VrCommon INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/../VrCommon/Inc
)
endif()
add_library(CloudView3D STATIC
Src/PointCloudGLWidget.cpp
Inc/CloudView3D.h
Inc/CloudView3DTypes.h
Inc/PointCloudGLWidget.h
)
set_target_properties(CloudView3D PROPERTIES
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
AUTOMOC ON
)
target_include_directories(CloudView3D PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/Inc
${CMAKE_CURRENT_SOURCE_DIR}/../VrCommon/Inc
)
target_link_libraries(CloudView3D PUBLIC
VrCommon
Qt5::Core
Qt5::Gui
Qt5::Widgets
Qt5::OpenGL
)
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)
endif()

View File

@ -1,30 +1,30 @@
QT += core gui widgets opengl
CONFIG += c++17 staticlib
TEMPLATE = lib
TARGET = CloudView3D
win32-msvc {
QMAKE_CXXFLAGS += /utf-8
QMAKE_CXXFLAGS += /bigobj
}
INCLUDEPATH += $$PWD/Inc
INCLUDEPATH += $$PWD/../VrUtils/Inc
INCLUDEPATH += $$PWD/../../Device/SDK/VzNLSDK/Inc
INCLUDEPATH += $$PWD/../VrCommon/Inc
HEADERS += \
Inc/CloudView3D.h \
Inc/CloudView3DTypes.h \
Inc/PointCloudGLWidget.h
SOURCES += \
Src/PointCloudGLWidget.cpp
win32 {
LIBS += -lopengl32
LIBS += -lglu32
}
DEFINES += _USE_MATH_DEFINES
QT += core gui widgets opengl
CONFIG += c++17 staticlib
TEMPLATE = lib
TARGET = CloudView3D
win32-msvc {
QMAKE_CXXFLAGS += /utf-8
QMAKE_CXXFLAGS += /bigobj
}
INCLUDEPATH += $$PWD/Inc
INCLUDEPATH += $$PWD/../VrUtils/Inc
INCLUDEPATH += $$PWD/../VrCommon/Inc
# VZNL_Types.h is maintained in the shared Utils module.
HEADERS += \
Inc/CloudView3D.h \
Inc/CloudView3DTypes.h \
Inc/PointCloudGLWidget.h
SOURCES += \
Src/PointCloudGLWidget.cpp
win32 {
LIBS += -lopengl32
LIBS += -lglu32
}
DEFINES += _USE_MATH_DEFINES

View File

@ -1,7 +1,7 @@
#ifndef CLOUD_VIEW_3D_H
#define CLOUD_VIEW_3D_H
#include "CloudView3DTypes.h"
#include "PointCloudGLWidget.h"
#endif // CLOUD_VIEW_3D_H
#ifndef CLOUD_VIEW_3D_H
#define CLOUD_VIEW_3D_H
#include "CloudView3DTypes.h"
#include "PointCloudGLWidget.h"
#endif // CLOUD_VIEW_3D_H

View File

@ -1,77 +1,77 @@
#ifndef CLOUD_VIEW_3D_TYPES_H
#define CLOUD_VIEW_3D_TYPES_H
#include <cstddef>
#include <cstdint>
#include <vector>
struct Point3D
{
float x, y, z;
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;
uint8_t r, g, b;
float pointSize;
Point3DRGB() : x(0), y(0), z(0), r(255), g(255), b(255), pointSize(0) {}
Point3DRGB(float _x,
float _y,
float _z,
uint8_t _r = 255,
uint8_t _g = 255,
uint8_t _b = 255,
float _ps = 0)
: x(_x), y(_y), z(_z), r(_r), g(_g), b(_b), pointSize(_ps) {}
};
template<typename PointT>
class SimplePointCloud
{
public:
std::vector<PointT> points;
std::vector<int> lineIndices;
// Each point's original position within its scan line (-1 when unknown).
std::vector<int> pointInLineIndices;
void clear()
{
points.clear();
lineIndices.clear();
pointInLineIndices.clear();
}
size_t size() const { return points.size(); }
bool empty() const { return points.empty(); }
void reserve(size_t n)
{
points.reserve(n);
lineIndices.reserve(n);
pointInLineIndices.reserve(n);
}
void push_back(const PointT& pt)
{
points.push_back(pt);
pointInLineIndices.push_back(-1);
}
void push_back(const PointT& pt, int lineIdx)
{
points.push_back(pt);
lineIndices.push_back(lineIdx);
pointInLineIndices.push_back(-1);
}
void push_back(const PointT& pt, int lineIdx, int pointInLineIdx)
{
points.push_back(pt);
lineIndices.push_back(lineIdx);
pointInLineIndices.push_back(pointInLineIdx);
}
};
using PointCloudXYZ = SimplePointCloud<Point3D>;
using PointCloudXYZRGB = SimplePointCloud<Point3DRGB>;
#endif // CLOUD_VIEW_3D_TYPES_H
#ifndef CLOUD_VIEW_3D_TYPES_H
#define CLOUD_VIEW_3D_TYPES_H
#include <cstddef>
#include <cstdint>
#include <vector>
struct Point3D
{
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; // millimetres
uint8_t r, g, b;
float pointSize;
Point3DRGB() : x(0), y(0), z(0), r(255), g(255), b(255), pointSize(0) {}
Point3DRGB(float _x,
float _y,
float _z,
uint8_t _r = 255,
uint8_t _g = 255,
uint8_t _b = 255,
float _ps = 0)
: x(_x), y(_y), z(_z), r(_r), g(_g), b(_b), pointSize(_ps) {}
};
template<typename PointT>
class SimplePointCloud
{
public:
std::vector<PointT> points;
std::vector<int> lineIndices;
// Each point's original position within its scan line (-1 when unknown).
std::vector<int> pointInLineIndices;
void clear()
{
points.clear();
lineIndices.clear();
pointInLineIndices.clear();
}
size_t size() const { return points.size(); }
bool empty() const { return points.empty(); }
void reserve(size_t n)
{
points.reserve(n);
lineIndices.reserve(n);
pointInLineIndices.reserve(n);
}
void push_back(const PointT& pt)
{
points.push_back(pt);
pointInLineIndices.push_back(-1);
}
void push_back(const PointT& pt, int lineIdx)
{
points.push_back(pt);
lineIndices.push_back(lineIdx);
pointInLineIndices.push_back(-1);
}
void push_back(const PointT& pt, int lineIdx, int pointInLineIdx)
{
points.push_back(pt);
lineIndices.push_back(lineIdx);
pointInLineIndices.push_back(pointInLineIdx);
}
};
using PointCloudXYZ = SimplePointCloud<Point3D>;
using PointCloudXYZRGB = SimplePointCloud<Point3DRGB>;
#endif // CLOUD_VIEW_3D_TYPES_H

View File

@ -6,9 +6,10 @@
#include <QOpenGLBuffer>
#include <QOpenGLShaderProgram>
#include <QMatrix4x4>
#include <QVector3D>
#include <QQuaternion>
#include <QMouseEvent>
#include <QVector3D>
#include <QQuaternion>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QWheelEvent>
#include <QResizeEvent>
#include <deque>
@ -18,9 +19,10 @@
#include "VZNL_Types.h"
class QToolButton;
class QPushButton;
class QPinchGesture;
class QResizeEvent;
class QPushButton;
class QPinchGesture;
class QResizeEvent;
class QTimer;
/**
* @brief 点云显示颜色枚举
@ -185,10 +187,30 @@ public:
void addPointCloud(const PointCloudXYZ& cloud, const QString& name = "", bool resetView = true);
void addPointCloud(const PointCloudXYZRGB& cloud, const QString& name = "", bool resetView = true);
/** 删除最早添加的一组点云,用于维护滚动显示窗口。 */
bool removeOldestPointCloud();
bool removeOldestPointCloud(bool refreshDisplay = true);
/** 删除最近添加的一组点云,用于回滚未完成的增量加载。 */
bool removeNewestPointCloud();
void clearPointClouds();
bool removeNewestPointCloud(bool refreshDisplay = true);
void clearPointClouds();
/** 配置流式点云刷新频率;CamTool 使用 30 Hz。 */
void setStreamRefreshRate(int refreshRateHz);
int streamRefreshRate() const
{
return m_streamRefreshRate;
}
/** 配置流式点云最多保留的源扫描线数。 */
void setStreamLineLimit(int maxLines);
int streamLineLimit() const
{
return m_streamLineLimit;
}
void startStreamRefresh();
void stopStreamRefresh(bool flushPending = true);
/** 缓存一批本地线号从 0 开始的扫描线,下一次刷新时统一提交。 */
void queuePointCloudBatch(PointCloudXYZ cloud, int sourceLineCount, const QString& name = QString());
void flushPointCloudBatches();
void clearPointCloudBatches();
void setPointCloudColor(PointCloudColor color);
void setPointSize(float size);
@ -391,8 +413,10 @@ signals:
float dx,
float dy,
float dz);
void lineSelected(const SelectedLineInfo& line);
void viewAnglesChanged(float rotX, float rotY, float rotZ);
void lineSelected(const SelectedLineInfo& line);
void viewAnglesChanged(float rotX, float rotY, float rotZ);
/** 30 Hz 刷新边界;数据源应在该信号处理中批量填充缓存。 */
void streamRefreshRequested();
protected:
bool event(QEvent* event) override;
@ -407,8 +431,15 @@ protected:
void keyPressEvent(QKeyEvent* event) override;
void resizeEvent(QResizeEvent* event) override;
private:
struct PointCloudData
private:
struct QueuedPointCloudBatch
{
PointCloudXYZ cloud;
QString name;
int sourceLineCount;
};
struct PointCloudData
{
std::vector<float> vertices;
std::vector<float> colors;
@ -419,9 +450,11 @@ private:
bool pseudoColor;
bool hasLineInfo; // 是否有线信息
QString name;
int colorIndex; // 点云颜色索引
int totalLines; // 总线数
int pointsPerLine; // 每线点数(网格化点云)
int colorIndex; // 点云颜色索引
int totalLines; // 总线数
int pointsPerLine; // 每线点数(网格化点云)
bool streamBatch;
int sourceLineCount;
// 自定义点大小分组(RGBA 中 A > 1 的点按大小分组)
struct PointSizeGroup {
@ -435,14 +468,19 @@ private:
QOpenGLBuffer vertexBuffer; // 顶点 VBO
QOpenGLBuffer colorBuffer; // 颜色 VBO
bool vboCreated; // VBO 是否已创建
QVector3D minBound;
QVector3D maxBound;
bool hasBounds;
PointCloudData()
: hasColor(false), pseudoColor(false), hasLineInfo(false), colorIndex(0)
, totalLines(0), pointsPerLine(0)
, hasCustomPointSizes(false)
: hasColor(false), pseudoColor(false), hasLineInfo(false), colorIndex(0)
, totalLines(0), pointsPerLine(0)
, streamBatch(false), sourceLineCount(0)
, hasCustomPointSizes(false)
, vertexBuffer(QOpenGLBuffer::VertexBuffer)
, colorBuffer(QOpenGLBuffer::VertexBuffer)
, vboCreated(false)
, hasBounds(false)
{}
// 移动构造函数(QOpenGLBuffer 不支持拷贝)
@ -456,14 +494,19 @@ private:
, pseudoColor(other.pseudoColor)
, hasLineInfo(other.hasLineInfo)
, name(std::move(other.name))
, colorIndex(other.colorIndex)
, totalLines(other.totalLines)
, pointsPerLine(other.pointsPerLine)
, customPointSizeGroups(std::move(other.customPointSizeGroups))
, colorIndex(other.colorIndex)
, totalLines(other.totalLines)
, pointsPerLine(other.pointsPerLine)
, streamBatch(other.streamBatch)
, sourceLineCount(other.sourceLineCount)
, customPointSizeGroups(std::move(other.customPointSizeGroups))
, hasCustomPointSizes(other.hasCustomPointSizes)
, vertexBuffer(QOpenGLBuffer::VertexBuffer)
, colorBuffer(QOpenGLBuffer::VertexBuffer)
, vboCreated(false)
, minBound(other.minBound)
, maxBound(other.maxBound)
, hasBounds(other.hasBounds)
{
// VBO 需要在 GL 上下文中重建,标记为未创建
other.vboCreated = false;
@ -487,11 +530,16 @@ private:
pseudoColor = other.pseudoColor;
hasLineInfo = other.hasLineInfo;
name = std::move(other.name);
colorIndex = other.colorIndex;
totalLines = other.totalLines;
pointsPerLine = other.pointsPerLine;
customPointSizeGroups = std::move(other.customPointSizeGroups);
colorIndex = other.colorIndex;
totalLines = other.totalLines;
pointsPerLine = other.pointsPerLine;
streamBatch = other.streamBatch;
sourceLineCount = other.sourceLineCount;
customPointSizeGroups = std::move(other.customPointSizeGroups);
hasCustomPointSizes = other.hasCustomPointSizes;
minBound = other.minBound;
maxBound = other.maxBound;
hasBounds = other.hasBounds;
// VBO 需要重建
other.vboCreated = false;
}
@ -507,6 +555,8 @@ private:
void updateProjection(const QMatrix4x4& modelView);
float fittedCameraDistance(float padding = 1.05f) const;
float minimumCameraDistance() const;
/** 按当前缩放把点大小从逻辑像素换到实际绘制像素,避免放大后点散成看不见的碎点。 */
float effectivePointSize() const;
QVector3D colorForIndex(int colorIndex) const;
SelectedPointInfo pickPoint(int screenX, int screenY);
void drawSelectedPoints();
@ -519,15 +569,26 @@ private:
void drawBasicShapeSegments(); // 绘制基础图形
void drawBasicShapePoints();
void drawLineSegmentList(const QVector<LineSegment>& segments);
void drawPosePoints(); // 绘制姿态点
void drawAxisLabels(); // 绘制坐标轴 XYZ 标注
void drawSelectionInfoOverlay(); // 右上角显示选点和测量结果
QVector3D pseudoColorForZ(float z) const;
void rebuildPseudoColors(PointCloudData& data, size_t firstVertex);
void updatePseudoColors();
void createControlButtons();
void updateControlButtonGeometry();
void uploadToVBO(PointCloudData& data); // 上传数据到 VBO
void releaseVBO(PointCloudData& data); // 释放 VBO 资源
void uploadToVBO(PointCloudData& data); // 上传数据到 VBO
void releaseVBO(PointCloudData& data); // 释放 VBO 资源
void expandCloudBounds(PointCloudData& data, float x, float y, float z);
void addPointCloudInternal(const PointCloudXYZ& cloud, const QString& name, bool resetViewAfterAdd, bool refreshDisplay);
void trimQueuedPointCloudBatches();
void trimDisplayedStreamBatches(bool refreshDisplay);
bool removeOldestStreamPointCloud(bool refreshDisplay);
void discardLeadingLines(QueuedPointCloudBatch& batch, int lineCount);
void discardLeadingStreamLines(PointCloudData& data, int lineCount);
void recomputeCloudBounds(PointCloudData& data);
std::deque<PointCloudData> m_pointClouds;
@ -623,8 +684,16 @@ private:
QVector<QToolButton*> m_viewButtons;
QPushButton* m_viewAngleButton; // 左下角旋转角度显示按钮
QToolButton* m_zoomInButton;
QToolButton* m_zoomOutButton;
};
QToolButton* m_zoomInButton;
QToolButton* m_zoomOutButton;
// 高速输入先进入 CPU 有界缓存,再由 GUI 线程按固定频率批量提交 OpenGL。
QTimer* m_streamRefreshTimer;
std::deque<QueuedPointCloudBatch> m_queuedPointCloudBatches;
int m_queuedPointCloudLineCount;
int m_streamDisplayedLineCount;
int m_streamRefreshRate;
int m_streamLineLimit;
};
#endif // POINT_CLOUD_GL_WIDGET_H

File diff suppressed because it is too large Load Diff

View File

@ -83,9 +83,10 @@ private:
QString fileName;
};
void buildStartupSequence(const StartupFiles& files);
bool loadStartupItem(const StartupItem& item);
bool loadCurrentStartupSequenceItem();
void buildStartupSequence(const StartupFiles& files);
bool loadStartupItem(const StartupItem& item);
bool loadCurrentStartupSequenceItem();
void updateZoomButtonGeometry();
private slots:
/**

View File

@ -1,27 +1,27 @@
#ifndef CLOUD_VIEW_SETTINGS_H
#define CLOUD_VIEW_SETTINGS_H
#include <QVector>
#include <QVector3D>
#include "PointCloudXYZRange.h"
struct CloudViewSettingsData
{
float poseAxisLength = 10.0f;
bool pseudoColorEnabled = true;
float pseudoColorPeriod = 500.0f;
QVector3D pseudoColorStartColor = QVector3D(0.0f, 0.0f, 1.0f);
QVector3D pseudoColorEndColor = QVector3D(1.0f, 0.0f, 0.0f);
bool pseudoColorDefaultPalette = true;
PointCloudXYZRange pointCloudRange;
};
class CloudViewSettings
{
public:
static CloudViewSettingsData load();
static bool save(const CloudViewSettingsData& settings);
};
#endif // CLOUD_VIEW_SETTINGS_H
#ifndef CLOUD_VIEW_SETTINGS_H
#define CLOUD_VIEW_SETTINGS_H
#include <QVector>
#include <QVector3D>
#include "PointCloudXYZRange.h"
struct CloudViewSettingsData
{
float poseAxisLength = 10.0f;
bool pseudoColorEnabled = true;
float pseudoColorPeriod = 500.0f;
QVector3D pseudoColorStartColor = QVector3D(0.0f, 0.0f, 1.0f);
QVector3D pseudoColorEndColor = QVector3D(1.0f, 0.0f, 0.0f);
bool pseudoColorDefaultPalette = true;
PointCloudXYZRange pointCloudRange;
};
class CloudViewSettings
{
public:
static CloudViewSettingsData load();
static bool save(const CloudViewSettingsData& settings);
};
#endif // CLOUD_VIEW_SETTINGS_H

View File

@ -1,6 +1,6 @@
#ifndef CLOUD_VIEW_POINT_CLOUD_GL_WIDGET_FORWARDER_H
#define CLOUD_VIEW_POINT_CLOUD_GL_WIDGET_FORWARDER_H
#include "../../CloudView3D/Inc/PointCloudGLWidget.h"
#endif // CLOUD_VIEW_POINT_CLOUD_GL_WIDGET_FORWARDER_H
#ifndef CLOUD_VIEW_POINT_CLOUD_GL_WIDGET_FORWARDER_H
#define CLOUD_VIEW_POINT_CLOUD_GL_WIDGET_FORWARDER_H
#include "../../CloudView3D/Inc/PointCloudGLWidget.h"
#endif // CLOUD_VIEW_POINT_CLOUD_GL_WIDGET_FORWARDER_H

View File

@ -1245,24 +1245,40 @@ void CloudViewMainWindow::setPointCloudRange(const PointCloudXYZRange& range)
}
}
bool CloudViewMainWindow::eventFilter(QObject* obj, QEvent* event)
{
if (obj == m_glWidget && event->type() == QEvent::Resize &&
m_btnZoomIn && m_btnZoomOut) {
const auto* resizeEvent = static_cast<QResizeEvent*>(event);
const int width = resizeEvent->size().width();
const int height = resizeEvent->size().height();
const int buttonWidth = m_btnZoomIn->width();
const int buttonHeight = m_btnZoomIn->height();
const int margin = 8;
const int gap = 4;
const int x = width - buttonWidth - margin;
const int y = qMax(0, (height - buttonHeight * 2 - gap) / 2);
m_btnZoomIn->move(x, y);
m_btnZoomOut->move(x, y + buttonHeight + gap);
}
return QMainWindow::eventFilter(obj, event);
}
bool CloudViewMainWindow::eventFilter(QObject* obj, QEvent* event)
{
if (obj == m_glWidget && event->type() == QEvent::Resize &&
m_btnZoomIn && m_btnZoomOut) {
updateZoomButtonGeometry();
}
return QMainWindow::eventFilter(obj, event);
}
void CloudViewMainWindow::updateZoomButtonGeometry()
{
if (!m_glWidget || !m_btnZoomIn || !m_btnZoomOut)
{
return;
}
const int widgetWidth = m_glWidget->width();
const int widgetHeight = m_glWidget->height();
if (widgetWidth <= 0 || widgetHeight <= 0)
{
return;
}
const int buttonWidth = m_btnZoomIn->width();
const int buttonHeight = m_btnZoomIn->height();
const int margin = 8;
const int gap = 4;
const int x = qMax(0, widgetWidth - buttonWidth - margin);
const int y = qMax(0, (widgetHeight - buttonHeight * 2 - gap) / 2);
m_btnZoomIn->move(x, y);
m_btnZoomOut->move(x, y + buttonHeight + gap);
m_btnZoomIn->raise();
m_btnZoomOut->raise();
}
void CloudViewMainWindow::closeEvent(QCloseEvent* event)
{
@ -1417,11 +1433,13 @@ QWidget* CloudViewMainWindow::createViewerArea()
m_btnZoomOut->setAutoRepeat(true);
m_btnZoomOut->setAutoRepeatDelay(300);
m_btnZoomOut->setAutoRepeatInterval(50);
connect(m_btnZoomOut, &QToolButton::clicked, m_glWidget, &PointCloudGLWidget::zoomOut);
m_glWidget->installEventFilter(this);
// 连接信号
connect(m_btnZoomOut, &QToolButton::clicked, m_glWidget, &PointCloudGLWidget::zoomOut);
m_glWidget->installEventFilter(this);
// 初始创建时可能没有收到 resize 事件,立即将按钮放到视口右侧居中。
updateZoomButtonGeometry();
// 连接信号
connect(m_glWidget, &PointCloudGLWidget::pointSelected, this, &CloudViewMainWindow::onPointSelected);
connect(m_glWidget, &PointCloudGLWidget::twoPointsSelected, this, &CloudViewMainWindow::onTwoPointsSelected);
connect(m_glWidget, &PointCloudGLWidget::lineSelected, this, &CloudViewMainWindow::onLineSelected);

View File

@ -1,171 +1,171 @@
#include "CloudViewSettings.h"
#include <QColor>
#include <QDir>
#include <QSettings>
#include <QStandardPaths>
#include <cmath>
namespace
{
QString settingsFilePath()
{
QString settingsDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
if (settingsDir.isEmpty()) {
settingsDir = QDir::homePath() + "/CloudView";
}
QDir().mkpath(settingsDir);
return QDir(settingsDir).filePath("CloudView.ini");
}
}
CloudViewSettingsData CloudViewSettings::load()
{
CloudViewSettingsData result;
QSettings settings(settingsFilePath(), QSettings::IniFormat);
// 模式独立于启用状态保存;缺失或非法配置默认采用“替换为全 0”。
const QVariant savedModeValue = settings.contains("PointCloudRange/filterMode")
? settings.value("PointCloudRange/filterMode")
: settings.value("PointCloudRange/mode",
static_cast<int>(PointCloudXYZRangeFilterMode::ReplaceWithZero));
const int savedFilterMode = savedModeValue.toInt();
const PointCloudXYZRangeFilterMode filterMode =
savedFilterMode == static_cast<int>(PointCloudXYZRangeFilterMode::Remove)
? PointCloudXYZRangeFilterMode::Remove
: PointCloudXYZRangeFilterMode::ReplaceWithZero;
const float poseAxisLength = settings.value("Display/poseAxisLength", result.poseAxisLength).toFloat();
if (std::isfinite(poseAxisLength) && poseAxisLength > 0.0f) {
result.poseAxisLength = poseAxisLength;
}
result.pseudoColorEnabled = settings.value("PseudoColor/enabled", result.pseudoColorEnabled).toBool();
const bool hasPaletteSetting = settings.contains("PseudoColor/defaultPalette");
result.pseudoColorDefaultPalette = hasPaletteSetting
? settings.value("PseudoColor/defaultPalette").toBool()
: result.pseudoColorDefaultPalette;
const float period = settings.value("PseudoColor/period", result.pseudoColorPeriod).toFloat();
if (std::isfinite(period) && period > 0.0f) {
result.pseudoColorPeriod = period;
}
const QColor startColor(settings.value("PseudoColor/startColor", "#0000ff").toString());
const QColor endColor(settings.value("PseudoColor/endColor", "#ff0000").toString());
if (startColor.isValid()) {
result.pseudoColorStartColor = QVector3D(startColor.redF(), startColor.greenF(), startColor.blueF());
}
if (endColor.isValid()) {
result.pseudoColorEndColor = QVector3D(endColor.redF(), endColor.greenF(), endColor.blueF());
}
if (!hasPaletteSetting && settings.contains("PseudoColor/period")) {
const QColor legacyStartColor(settings.value("PseudoColor/startColor", "#0000ff").toString());
const QColor legacyEndColor(settings.value("PseudoColor/endColor", "#ff0000").toString());
const bool legacyDefaults = qFuzzyCompare(result.pseudoColorPeriod, 100.0f) &&
legacyStartColor == QColor("#0000ff") &&
legacyEndColor == QColor("#ff0000");
if (legacyDefaults) {
// 旧版周期色环配置迁移为 Z 范围蓝→红梯度。
result.pseudoColorPeriod = 500.0f;
result.pseudoColorStartColor = QVector3D(0.0f, 0.0f, 1.0f);
result.pseudoColorEndColor = QVector3D(1.0f, 0.0f, 0.0f);
result.pseudoColorDefaultPalette = true;
} else {
// 非默认旧配置继续使用原有的自定义起止色渐变。
result.pseudoColorDefaultPalette = false;
}
}
const bool rangeEnabled = settings.value("PointCloudRange/enabled", false).toBool();
PointCloudXYZRange range;
range.enabled = rangeEnabled;
range.filterMode = filterMode;
if (rangeEnabled) {
const auto readBound = [&settings](const char* key, double fallback) {
bool ok = false;
const double value = settings.value(key).toDouble(&ok);
return ok && std::isfinite(value) ? value : fallback;
};
// Missing bounds retain the range object's implicit unbounded side.
range.minX = readBound("PointCloudRange/minX", range.minX);
range.maxX = readBound("PointCloudRange/maxX", range.maxX);
range.minY = readBound("PointCloudRange/minY", range.minY);
range.maxY = readBound("PointCloudRange/maxY", range.maxY);
range.minZ = readBound("PointCloudRange/minZ", range.minZ);
range.maxZ = readBound("PointCloudRange/maxZ", range.maxZ);
if (range.isValid()) {
result.pointCloudRange = range;
}
} else {
result.pointCloudRange = range;
}
// Keep the mode even when a persisted enabled range is invalid and is
// therefore reset to the disabled default range.
result.pointCloudRange.filterMode = filterMode;
return result;
}
bool CloudViewSettings::save(const CloudViewSettingsData& value)
{
QSettings settings(settingsFilePath(), QSettings::IniFormat);
if (std::isfinite(value.poseAxisLength) && value.poseAxisLength > 0.0f) {
settings.setValue("Display/poseAxisLength", value.poseAxisLength);
}
settings.setValue("PseudoColor/enabled", value.pseudoColorEnabled);
settings.setValue("PseudoColor/defaultPalette", value.pseudoColorDefaultPalette);
settings.setValue("PseudoColor/period", qMax(1.0f, value.pseudoColorPeriod));
settings.setValue("PseudoColor/startColor",
QColor::fromRgbF(value.pseudoColorStartColor.x(),
value.pseudoColorStartColor.y(),
value.pseudoColorStartColor.z()).name(QColor::HexRgb));
settings.setValue("PseudoColor/endColor",
QColor::fromRgbF(value.pseudoColorEndColor.x(),
value.pseudoColorEndColor.y(),
value.pseudoColorEndColor.z()).name(QColor::HexRgb));
const PointCloudXYZRange& range = value.pointCloudRange;
const bool saveRange = range.enabled && range.isValid();
settings.setValue("PointCloudRange/enabled", saveRange);
const int filterModeValue = range.filterMode == PointCloudXYZRangeFilterMode::Remove
? static_cast<int>(PointCloudXYZRangeFilterMode::Remove)
: static_cast<int>(PointCloudXYZRangeFilterMode::ReplaceWithZero);
settings.setValue("PointCloudRange/filterMode", filterModeValue);
settings.setValue("PointCloudRange/mode", filterModeValue);
if (saveRange) {
const auto saveBound = [&settings](const char* key, double bound) {
if (std::isfinite(bound)) {
settings.setValue(key, bound);
} else {
settings.remove(key);
}
};
saveBound("PointCloudRange/minX", range.minX);
saveBound("PointCloudRange/maxX", range.maxX);
saveBound("PointCloudRange/minY", range.minY);
saveBound("PointCloudRange/maxY", range.maxY);
saveBound("PointCloudRange/minZ", range.minZ);
saveBound("PointCloudRange/maxZ", range.maxZ);
} else {
// Disabled ranges use implicit infinite bounds; do not persist infinities.
settings.remove("PointCloudRange/minX");
settings.remove("PointCloudRange/maxX");
settings.remove("PointCloudRange/minY");
settings.remove("PointCloudRange/maxY");
settings.remove("PointCloudRange/minZ");
settings.remove("PointCloudRange/maxZ");
}
// Remove keys from the former min/max/stop based implementation so old
// values cannot reappear in the user configuration file.
settings.remove("PseudoColor/autoRange");
settings.remove("PseudoColor/minZ");
settings.remove("PseudoColor/maxZ");
settings.remove("PseudoColor/divisionCount");
settings.remove("PseudoColor/gradient");
settings.remove("PseudoColor/stops");
settings.sync();
return settings.status() == QSettings::NoError;
}
#include "CloudViewSettings.h"
#include <QColor>
#include <QDir>
#include <QSettings>
#include <QStandardPaths>
#include <cmath>
namespace
{
QString settingsFilePath()
{
QString settingsDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
if (settingsDir.isEmpty()) {
settingsDir = QDir::homePath() + "/CloudView";
}
QDir().mkpath(settingsDir);
return QDir(settingsDir).filePath("CloudView.ini");
}
}
CloudViewSettingsData CloudViewSettings::load()
{
CloudViewSettingsData result;
QSettings settings(settingsFilePath(), QSettings::IniFormat);
// 模式独立于启用状态保存;缺失或非法配置默认采用“替换为全 0”。
const QVariant savedModeValue = settings.contains("PointCloudRange/filterMode")
? settings.value("PointCloudRange/filterMode")
: settings.value("PointCloudRange/mode",
static_cast<int>(PointCloudXYZRangeFilterMode::ReplaceWithZero));
const int savedFilterMode = savedModeValue.toInt();
const PointCloudXYZRangeFilterMode filterMode =
savedFilterMode == static_cast<int>(PointCloudXYZRangeFilterMode::Remove)
? PointCloudXYZRangeFilterMode::Remove
: PointCloudXYZRangeFilterMode::ReplaceWithZero;
const float poseAxisLength = settings.value("Display/poseAxisLength", result.poseAxisLength).toFloat();
if (std::isfinite(poseAxisLength) && poseAxisLength > 0.0f) {
result.poseAxisLength = poseAxisLength;
}
result.pseudoColorEnabled = settings.value("PseudoColor/enabled", result.pseudoColorEnabled).toBool();
const bool hasPaletteSetting = settings.contains("PseudoColor/defaultPalette");
result.pseudoColorDefaultPalette = hasPaletteSetting
? settings.value("PseudoColor/defaultPalette").toBool()
: result.pseudoColorDefaultPalette;
const float period = settings.value("PseudoColor/period", result.pseudoColorPeriod).toFloat();
if (std::isfinite(period) && period > 0.0f) {
result.pseudoColorPeriod = period;
}
const QColor startColor(settings.value("PseudoColor/startColor", "#0000ff").toString());
const QColor endColor(settings.value("PseudoColor/endColor", "#ff0000").toString());
if (startColor.isValid()) {
result.pseudoColorStartColor = QVector3D(startColor.redF(), startColor.greenF(), startColor.blueF());
}
if (endColor.isValid()) {
result.pseudoColorEndColor = QVector3D(endColor.redF(), endColor.greenF(), endColor.blueF());
}
if (!hasPaletteSetting && settings.contains("PseudoColor/period")) {
const QColor legacyStartColor(settings.value("PseudoColor/startColor", "#0000ff").toString());
const QColor legacyEndColor(settings.value("PseudoColor/endColor", "#ff0000").toString());
const bool legacyDefaults = qFuzzyCompare(result.pseudoColorPeriod, 100.0f) &&
legacyStartColor == QColor("#0000ff") &&
legacyEndColor == QColor("#ff0000");
if (legacyDefaults) {
// 旧版周期色环配置迁移为 Z 范围蓝→红梯度。
result.pseudoColorPeriod = 500.0f;
result.pseudoColorStartColor = QVector3D(0.0f, 0.0f, 1.0f);
result.pseudoColorEndColor = QVector3D(1.0f, 0.0f, 0.0f);
result.pseudoColorDefaultPalette = true;
} else {
// 非默认旧配置继续使用原有的自定义起止色渐变。
result.pseudoColorDefaultPalette = false;
}
}
const bool rangeEnabled = settings.value("PointCloudRange/enabled", false).toBool();
PointCloudXYZRange range;
range.enabled = rangeEnabled;
range.filterMode = filterMode;
if (rangeEnabled) {
const auto readBound = [&settings](const char* key, double fallback) {
bool ok = false;
const double value = settings.value(key).toDouble(&ok);
return ok && std::isfinite(value) ? value : fallback;
};
// Missing bounds retain the range object's implicit unbounded side.
range.minX = readBound("PointCloudRange/minX", range.minX);
range.maxX = readBound("PointCloudRange/maxX", range.maxX);
range.minY = readBound("PointCloudRange/minY", range.minY);
range.maxY = readBound("PointCloudRange/maxY", range.maxY);
range.minZ = readBound("PointCloudRange/minZ", range.minZ);
range.maxZ = readBound("PointCloudRange/maxZ", range.maxZ);
if (range.isValid()) {
result.pointCloudRange = range;
}
} else {
result.pointCloudRange = range;
}
// Keep the mode even when a persisted enabled range is invalid and is
// therefore reset to the disabled default range.
result.pointCloudRange.filterMode = filterMode;
return result;
}
bool CloudViewSettings::save(const CloudViewSettingsData& value)
{
QSettings settings(settingsFilePath(), QSettings::IniFormat);
if (std::isfinite(value.poseAxisLength) && value.poseAxisLength > 0.0f) {
settings.setValue("Display/poseAxisLength", value.poseAxisLength);
}
settings.setValue("PseudoColor/enabled", value.pseudoColorEnabled);
settings.setValue("PseudoColor/defaultPalette", value.pseudoColorDefaultPalette);
settings.setValue("PseudoColor/period", qMax(1.0f, value.pseudoColorPeriod));
settings.setValue("PseudoColor/startColor",
QColor::fromRgbF(value.pseudoColorStartColor.x(),
value.pseudoColorStartColor.y(),
value.pseudoColorStartColor.z()).name(QColor::HexRgb));
settings.setValue("PseudoColor/endColor",
QColor::fromRgbF(value.pseudoColorEndColor.x(),
value.pseudoColorEndColor.y(),
value.pseudoColorEndColor.z()).name(QColor::HexRgb));
const PointCloudXYZRange& range = value.pointCloudRange;
const bool saveRange = range.enabled && range.isValid();
settings.setValue("PointCloudRange/enabled", saveRange);
const int filterModeValue = range.filterMode == PointCloudXYZRangeFilterMode::Remove
? static_cast<int>(PointCloudXYZRangeFilterMode::Remove)
: static_cast<int>(PointCloudXYZRangeFilterMode::ReplaceWithZero);
settings.setValue("PointCloudRange/filterMode", filterModeValue);
settings.setValue("PointCloudRange/mode", filterModeValue);
if (saveRange) {
const auto saveBound = [&settings](const char* key, double bound) {
if (std::isfinite(bound)) {
settings.setValue(key, bound);
} else {
settings.remove(key);
}
};
saveBound("PointCloudRange/minX", range.minX);
saveBound("PointCloudRange/maxX", range.maxX);
saveBound("PointCloudRange/minY", range.minY);
saveBound("PointCloudRange/maxY", range.maxY);
saveBound("PointCloudRange/minZ", range.minZ);
saveBound("PointCloudRange/maxZ", range.maxZ);
} else {
// Disabled ranges use implicit infinite bounds; do not persist infinities.
settings.remove("PointCloudRange/minX");
settings.remove("PointCloudRange/maxX");
settings.remove("PointCloudRange/minY");
settings.remove("PointCloudRange/maxY");
settings.remove("PointCloudRange/minZ");
settings.remove("PointCloudRange/maxZ");
}
// Remove keys from the former min/max/stop based implementation so old
// values cannot reappear in the user configuration file.
settings.remove("PseudoColor/autoRange");
settings.remove("PseudoColor/minZ");
settings.remove("PseudoColor/maxZ");
settings.remove("PseudoColor/divisionCount");
settings.remove("PseudoColor/gradient");
settings.remove("PseudoColor/stops");
settings.sync();
return settings.status() == QSettings::NoError;
}

View File

@ -1,102 +1,102 @@
#include <QApplication>
#include <QSurfaceFormat>
#include <QIcon>
#include <QCommandLineParser>
#include <QTimer>
#include "CloudViewMainWindow.h"
#define APP_VERSION "1.2.1"
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
// 设置 OpenGL 格式 - 使用 2.1 版本以确保固定管线兼容
QSurfaceFormat format;
format.setDepthBufferSize(24);
format.setStencilBufferSize(8);
format.setVersion(2, 1); // 使用 OpenGL 2.1 以确保固定管线函数可用
format.setSamples(4); // 抗锯齿
QSurfaceFormat::setDefaultFormat(format);
// 设置应用信息
app.setApplicationName("CloudViewApp");
app.setOrganizationName("CloudViewApp");
app.setApplicationVersion(APP_VERSION);
QCommandLineParser parser;
parser.setApplicationDescription("CloudView point cloud and shape viewer");
parser.addHelpOption();
parser.addVersionOption();
QCommandLineOption cloudOption(
QStringList() << "cloud" << "point-cloud",
"Load a point cloud or segment file. Positional files are treated the same way.",
"file");
QCommandLineOption bboxOption(
QStringList() << "bbox",
"Load a bounding-box JSON file.",
"file");
QCommandLineOption circleOption(
QStringList() << "circle",
"Load a circle data file. Row format: {x,y,z}-{r,p,y}-{radius}.",
"file");
QCommandLineOption rectangleOption(
QStringList() << "rectangle",
"Load a rectangle data file. Row format: {x,y,z}-{r,p,y}-{width,height}.",
"file");
QCommandLineOption surfaceOption(
QStringList() << "surface",
"Load a surface data file. Row format: {x,y,z}-{length,width} or {x,y,z}-{r,p,y}-{length,width}.",
"file");
QCommandLineOption triangleOption(
QStringList() << "triangle",
"Load a triangle data file. Row format: {x,y,z}-{x,y,z}-{x,y,z}.",
"file");
QCommandLineOption sequenceOption(
QStringList() << "sequence" << "sequential",
"Display startup files one at a time. Closing the window loads the next file.");
parser.addOption(cloudOption);
parser.addOption(bboxOption);
parser.addOption(circleOption);
parser.addOption(rectangleOption);
parser.addOption(surfaceOption);
parser.addOption(triangleOption);
parser.addOption(sequenceOption);
parser.addPositionalArgument("files", "Point cloud or segment files to load.");
if (app.arguments().contains(QStringLiteral("--help")) ||
app.arguments().contains(QStringLiteral("-h"))) {
parser.showHelp(0);
}
if (app.arguments().contains(QStringLiteral("--version")) ||
app.arguments().contains(QStringLiteral("-v"))) {
parser.showVersion();
}
parser.process(app);
CloudViewMainWindow::StartupFiles startupFiles;
startupFiles.displayFiles = parser.values(cloudOption);
startupFiles.displayFiles.append(parser.positionalArguments());
startupFiles.bboxFiles = parser.values(bboxOption);
startupFiles.circleFiles = parser.values(circleOption);
startupFiles.rectangleFiles = parser.values(rectangleOption);
startupFiles.surfaceFiles = parser.values(surfaceOption);
startupFiles.triangleFiles = parser.values(triangleOption);
startupFiles.sequential = parser.isSet(sequenceOption);
// 设置应用程序图标
app.setWindowIcon(QIcon(":/logo.png"));
// 创建并显示主窗口
CloudViewMainWindow mainWindow;
mainWindow.setWindowTitle(QString("点云查看器 v%1").arg(APP_VERSION));
mainWindow.resize(1280, 720);
mainWindow.show();
QTimer::singleShot(0, &mainWindow, [&mainWindow, startupFiles]() {
mainWindow.loadStartupFiles(startupFiles);
});
return app.exec();
}
#include <QApplication>
#include <QSurfaceFormat>
#include <QIcon>
#include <QCommandLineParser>
#include <QTimer>
#include "CloudViewMainWindow.h"
#define APP_VERSION "1.2.1"
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
// 设置 OpenGL 格式 - 使用 2.1 版本以确保固定管线兼容
QSurfaceFormat format;
format.setDepthBufferSize(24);
format.setStencilBufferSize(8);
format.setVersion(2, 1); // 使用 OpenGL 2.1 以确保固定管线函数可用
format.setSamples(4); // 抗锯齿
QSurfaceFormat::setDefaultFormat(format);
// 设置应用信息
app.setApplicationName("CloudViewApp");
app.setOrganizationName("CloudViewApp");
app.setApplicationVersion(APP_VERSION);
QCommandLineParser parser;
parser.setApplicationDescription("CloudView point cloud and shape viewer");
parser.addHelpOption();
parser.addVersionOption();
QCommandLineOption cloudOption(
QStringList() << "cloud" << "point-cloud",
"Load a point cloud or segment file. Positional files are treated the same way.",
"file");
QCommandLineOption bboxOption(
QStringList() << "bbox",
"Load a bounding-box JSON file.",
"file");
QCommandLineOption circleOption(
QStringList() << "circle",
"Load a circle data file. Row format: {x,y,z}-{r,p,y}-{radius}.",
"file");
QCommandLineOption rectangleOption(
QStringList() << "rectangle",
"Load a rectangle data file. Row format: {x,y,z}-{r,p,y}-{width,height}.",
"file");
QCommandLineOption surfaceOption(
QStringList() << "surface",
"Load a surface data file. Row format: {x,y,z}-{length,width} or {x,y,z}-{r,p,y}-{length,width}.",
"file");
QCommandLineOption triangleOption(
QStringList() << "triangle",
"Load a triangle data file. Row format: {x,y,z}-{x,y,z}-{x,y,z}.",
"file");
QCommandLineOption sequenceOption(
QStringList() << "sequence" << "sequential",
"Display startup files one at a time. Closing the window loads the next file.");
parser.addOption(cloudOption);
parser.addOption(bboxOption);
parser.addOption(circleOption);
parser.addOption(rectangleOption);
parser.addOption(surfaceOption);
parser.addOption(triangleOption);
parser.addOption(sequenceOption);
parser.addPositionalArgument("files", "Point cloud or segment files to load.");
if (app.arguments().contains(QStringLiteral("--help")) ||
app.arguments().contains(QStringLiteral("-h"))) {
parser.showHelp(0);
}
if (app.arguments().contains(QStringLiteral("--version")) ||
app.arguments().contains(QStringLiteral("-v"))) {
parser.showVersion();
}
parser.process(app);
CloudViewMainWindow::StartupFiles startupFiles;
startupFiles.displayFiles = parser.values(cloudOption);
startupFiles.displayFiles.append(parser.positionalArguments());
startupFiles.bboxFiles = parser.values(bboxOption);
startupFiles.circleFiles = parser.values(circleOption);
startupFiles.rectangleFiles = parser.values(rectangleOption);
startupFiles.surfaceFiles = parser.values(surfaceOption);
startupFiles.triangleFiles = parser.values(triangleOption);
startupFiles.sequential = parser.isSet(sequenceOption);
// 设置应用程序图标
app.setWindowIcon(QIcon(":/logo.png"));
// 创建并显示主窗口
CloudViewMainWindow mainWindow;
mainWindow.setWindowTitle(QString("点云查看器 v%1").arg(APP_VERSION));
mainWindow.resize(1280, 720);
mainWindow.show();
QTimer::singleShot(0, &mainWindow, [&mainWindow, startupFiles]() {
mainWindow.loadStartupFiles(startupFiles);
});
return app.exec();
}

View File

@ -1,17 +1,17 @@
QT += core gui widgets
TEMPLATE = lib
CONFIG += staticlib c++11
TARGET = ImageView
win32-msvc {
QMAKE_CXXFLAGS += /utf-8
}
INCLUDEPATH += $$PWD/Inc
HEADERS += \
Inc/ImageView.h
SOURCES += \
Src/ImageView.cpp
QT += core gui widgets
TEMPLATE = lib
CONFIG += staticlib c++11
TARGET = ImageView
win32-msvc {
QMAKE_CXXFLAGS += /utf-8
}
INCLUDEPATH += $$PWD/Inc
HEADERS += \
Inc/ImageView.h
SOURCES += \
Src/ImageView.cpp

View File

@ -1,83 +1,83 @@
#pragma once
#include <QGraphicsView>
#include <QImage>
#include <QPixmap>
#include <Qt>
class QLabel;
class QEvent;
class QGestureEvent;
class QGraphicsPixmapItem;
class QGraphicsScene;
class QMouseEvent;
class QPinchGesture;
class QWheelEvent;
// 可缩放、可平移的图片显示控件。
class ImageView : public QGraphicsView
{
Q_OBJECT
public:
explicit ImageView(QWidget* parent = nullptr);
~ImageView() override;
void setImage(const QImage& image);
void setImage(const QPixmap& pixmap);
bool loadImage(const QString& filePath);
void clearImage();
bool hasImage() const;
QImage image() const;
// 比例以原始图片大小为基准,1.0 表示 100%。
double zoomRatio() const;
void setZoomRatio(double ratio);
void zoomIn(double factor = 1.25);
void zoomOut(double factor = 1.25);
void resetView();
void setZoomRange(double minimum, double maximum);
double minimumZoomRatio() const;
double maximumZoomRatio() const;
// 图片未铺满视口时的停靠方向,默认居中。
void setImageAlignment(Qt::Alignment alignment);
Qt::Alignment imageAlignment() const;
signals:
void imageChanged();
void zoomRatioChanged(double ratio);
protected:
bool viewportEvent(QEvent* event) override;
void resizeEvent(QResizeEvent* event) override;
void wheelEvent(QWheelEvent* event) override;
void mousePressEvent(QMouseEvent* event) override;
void mouseMoveEvent(QMouseEvent* event) override;
void mouseReleaseEvent(QMouseEvent* event) override;
void mouseDoubleClickEvent(QMouseEvent* event) override;
private:
bool handleGestureEvent(QGestureEvent* event);
void handlePinchGesture(QPinchGesture* gesture);
void updateZoomLabel();
void updateZoomRatio(double ratio, bool keepCenter);
void updateZoomRatioAt(double ratio, const QPoint& viewportAnchor);
void repositionZoomLabel();
QGraphicsScene* m_scene;
QGraphicsPixmapItem* m_pixmapItem;
QLabel* m_zoomLabel;
QImage m_image;
double m_zoomRatio;
double m_minimumZoomRatio;
double m_maximumZoomRatio;
Qt::Alignment m_imageAlignment;
bool m_panning;
bool m_pinching;
bool m_needsInitialFit;
double m_pinchStartZoomRatio;
QPoint m_lastMousePosition;
};
#pragma once
#include <QGraphicsView>
#include <QImage>
#include <QPixmap>
#include <Qt>
class QLabel;
class QEvent;
class QGestureEvent;
class QGraphicsPixmapItem;
class QGraphicsScene;
class QMouseEvent;
class QPinchGesture;
class QWheelEvent;
// 可缩放、可平移的图片显示控件。
class ImageView : public QGraphicsView
{
Q_OBJECT
public:
explicit ImageView(QWidget* parent = nullptr);
~ImageView() override;
void setImage(const QImage& image);
void setImage(const QPixmap& pixmap);
bool loadImage(const QString& filePath);
void clearImage();
bool hasImage() const;
QImage image() const;
// 比例以原始图片大小为基准,1.0 表示 100%。
double zoomRatio() const;
void setZoomRatio(double ratio);
void zoomIn(double factor = 1.25);
void zoomOut(double factor = 1.25);
void resetView();
void setZoomRange(double minimum, double maximum);
double minimumZoomRatio() const;
double maximumZoomRatio() const;
// 图片未铺满视口时的停靠方向,默认居中。
void setImageAlignment(Qt::Alignment alignment);
Qt::Alignment imageAlignment() const;
signals:
void imageChanged();
void zoomRatioChanged(double ratio);
protected:
bool viewportEvent(QEvent* event) override;
void resizeEvent(QResizeEvent* event) override;
void wheelEvent(QWheelEvent* event) override;
void mousePressEvent(QMouseEvent* event) override;
void mouseMoveEvent(QMouseEvent* event) override;
void mouseReleaseEvent(QMouseEvent* event) override;
void mouseDoubleClickEvent(QMouseEvent* event) override;
private:
bool handleGestureEvent(QGestureEvent* event);
void handlePinchGesture(QPinchGesture* gesture);
void updateZoomLabel();
void updateZoomRatio(double ratio, bool keepCenter);
void updateZoomRatioAt(double ratio, const QPoint& viewportAnchor);
void repositionZoomLabel();
QGraphicsScene* m_scene;
QGraphicsPixmapItem* m_pixmapItem;
QLabel* m_zoomLabel;
QImage m_image;
double m_zoomRatio;
double m_minimumZoomRatio;
double m_maximumZoomRatio;
Qt::Alignment m_imageAlignment;
bool m_panning;
bool m_pinching;
bool m_needsInitialFit;
double m_pinchStartZoomRatio;
QPoint m_lastMousePosition;
};

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策略,允许AUTOMOC和AUTOUIC处理.hh文件
# cmake_policy(SET CMP0100 NEW)
INCLUDE_DIRECTORIES(
./_Inc
./Inc
./MD5
./tinyxml2
./log4cpp/include
)
AUX_SOURCE_DIRECTORY(./Src SrcS)
AUX_SOURCE_DIRECTORY(./MD5 MD5SrcS)
AUX_SOURCE_DIRECTORY(./tinyxml2 TinyxmlSrcS)
AUX_SOURCE_DIRECTORY(./log4cpp/src LogSrcS)
# 对特定的.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)
ADD_LIBRARY(VrUtils STATIC ${SrcS} ${MD5SrcS}
${TinyxmlSrcS}
${LogSrcS}
)
cmake_minimum_required(VERSION 3.14)
# 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
)
add_library(VrUtils STATIC ${VRUTILS_SOURCES})
set_target_properties(VrUtils PROPERTIES
CXX_STANDARD 11
CXX_STANDARD_REQUIRED ON
)
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()