1. FileIOFilter系统架构
1.1 FileIOFilter基类设计
核心文件 : CloudCompare/libs\qCC_io\include\FileIOFilter.h
FileIOFilter是所有I/O过滤器的抽象基类,采用策略模式设计:
cpp
class FileIOFilter {
public:
// ===== 核心加载接口 =====
virtual CC_FILE_ERROR loadFile(
const QString& filename, // 文件路径
ccHObject& container, // 输出容器
LoadParameters& parameters // 加载参数
);
// ===== 核心保存接口 =====
virtual CC_FILE_ERROR saveToFile(
ccHObject* entity, // 要保存的对象
const QString& filename, // 文件路径
const SaveParameters& parameters // 保存参数
);
// ===== 格式能力查询 =====
bool importSupported() const; // 是否支持导入
bool exportSupported() const; // 是否支持导出
virtual bool canSave(
CC_CLASS_ENUM type, // 对象类型
bool& multiple, // 是否支持多个
bool& exclusive // 是否独占
) const;
// ===== 过滤器信息 =====
QString getName() const; // 过滤器名称
float getPriority() const; // 优先级
QStringList getFileFilters() const; // 文件扩展名过滤器
protected:
FilterInfo m_filterInfo; // 过滤器元数据
};
1.2 LoadParameters结构
封装所有加载参数,支持全局坐标偏移和自动化处理:
cpp
struct LoadParameters {
// ===== 坐标偏移处理 =====
ccGlobalShiftManager::Mode shiftHandlingMode;
// 模式选项:
// - NO_DIALOG: 不处理
// - NO_DIALOG_AUTO_SHIFT: 自动偏移
// - DIALOG_IF_NECESSARY: 必要时对话框
// - ALWAYS_DISPLAY_DIALOG: 始终对话框
CCVector3d _coordinatesShift; // 坐标偏移向量
bool _coordinatesShiftEnabled; // 是否启用偏移
bool _coordinatesShiftForced; // 是否强制偏移
// ===== UI参数 =====
bool alwaysDisplayLoadDialog; // 始终显示加载对话框
QWidget* parentWidget; // 父窗口
bool autoComputeNormals; // 自动计算法向量
// ===== 会话管理 =====
bool sessionStart; // 是否为会话起始(用于"Apply All")
// ===== 其他 =====
bool preserveShiftOnSave; // 保存时保留偏移
};
1.3 错误码系统
详细的错误分类,便于调试和用户反馈:
cpp
enum CC_FILE_ERROR {
CC_FERR_NO_ERROR = 0, // 成功
CC_FERR_BAD_ARGUMENT, // 参数错误
CC_FERR_UNKNOWN_FILE, // 未知文件类型
CC_FERR_WRONG_FILE_TYPE, // 文件类型错误
CC_FERR_WRITING, // 写入错误
CC_FERR_READING, // 读取错误
CC_FERR_NO_SAVE, // 不支持保存
CC_FERR_NO_LOAD, // 不支持加载
CC_FERR_BAD_ENTITY_TYPE, // 实体类型错误
CC_FERR_CANCELED_BY_USER, // 用户取消
CC_FERR_NOT_ENOUGH_MEMORY, // 内存不足
CC_FERR_MALFORMED_FILE, // 文件格式错误
CC_FERR_CONSOLE_ERROR, // 控制台错误
CC_FERR_BROKEN_DEPENDENCY_ERROR, // 依赖关系错误
CC_FERR_FILE_WAS_WRITTEN_BY_UNKNOWN_PLUGIN, // 未知插件
CC_FERR_THIRD_PARTY_LIB_FAILURE, // 第三方库失败
CC_FERR_THIRD_PARTY_LIB_EXCEPTION // 第三方库异常
};
1.4 过滤器注册机制
静态注册表 + 优先级排序:
cpp
// 静态容器存储所有已注册过滤器
static std::vector<FileIOFilter::Shared> s_ioFilters;
// 注册过滤器(自动按优先级排序)
void FileIOFilter::Register(Shared filter) {
s_ioFilters.push_back(filter);
// 按优先级和ID排序(优先级高的在前)
std::sort(s_ioFilters.begin(), s_ioFilters.end(),
[](const Shared& a, const Shared& b) {
if (a->getPriority() != b->getPriority()) {
return a->getPriority() > b->getPriority();
}
return a->getID() < b->getID();
});
}
// 初始化内置过滤器
void FileIOFilter::InitInternalFilters() {
Register(Shared(new BinFilter())); // 优先级: 1.0
Register(Shared(new AsciiFilter())); // 优先级: 2.0
Register(Shared(new PlyFilter())); // 优先级: 7.0(最高)
Register(Shared(new DxfFilter()));
Register(Shared(new ShpFilter()));
Register(Shared(new RasterGridFilter()));
Register(Shared(new ImageFileFilter()));
}
// 根据扩展名查找最佳过滤器
FileIOFilter::Shared FindBestFilterForExtension(const QString& ext) {
const QString lowerExt = ext.toLower();
// 遍历已注册过滤器(已按优先级排序)
for (const auto& filter : s_ioFilters) {
if (filter->m_filterInfo.importExtensions.contains(lowerExt)) {
return filter; // 返回第一个匹配的(优先级最高)
}
}
return {};
}
设计优势:
- 运行时动态注册(插件可注册自己的过滤器)
- 优先级机制(PLY > ASCII > BIN)
- 扩展名自动匹配
2. 主要文件格式的解析
2.1 BIN格式 - CloudCompare原生格式
文件位置 : CloudCompare/libs\qCC_io\src\BinFilter.cpp (1381行)
版本演进
| 版本 | 特点 | 支持内容 |
|---|---|---|
| V1 (旧版) | 简单点云序列 | 点坐标、颜色、法线、标量场 |
| V2 (新版) | 完整对象序列化 | 复杂层次结构、所有对象类型 |
V2文件结构
┌─────────────────────────────────────┐
│ Header: 4字节 │
│ "CCB2" + 序列化标志 │
├─────────────────────────────────────┤
│ 版本号: 4字节 (uint32_t) │
├─────────────────────────────────────┤
│ 对象类型 (CC_CLASS_ENUM) │
├─────────────────────────────────────┤
│ 对象数据(递归序列化) │
│ ├─ 对象属性 │
│ ├─ 子对象 │
│ └─ 依赖对象引用 │
└─────────────────────────────────────┘
序列化标志 (Header第4字节):
cpp
enum DataFlag {
DF_POINT_COORDS_64_BITS = 1, // Bit 0: 坐标使用64位double
DF_SCALAR_VAL_32_BITS = 2 // Bit 1: 标量使用32位float
};
// 示例:flags = DF_POINT_COORDS_64_BITS | DF_SCALAR_VAL_32_BITS
加载流程 (LoadFileV2)
cpp
CC_FILE_ERROR BinFilter::LoadFileV2(QFile& in, ccHObject& container, ...) {
// 1. 读取版本号
uint32_t binVersion;
if (in.read((char*)&binVersion, 4) < 4) {
return CC_FERR_MALFORMED_FILE;
}
// 2. 读取对象类型
CC_CLASS_ENUM classID = ccObject::ReadClassIDFromFile(in, binVersion);
if (classID == CC_TYPES::OBJECT) {
return CC_FERR_MALFORMED_FILE;
}
// 3. 创建对象实例(工厂模式)
ccHObject* root = ccHObject::New(classID);
if (!root) {
return CC_FERR_BAD_ENTITY_TYPE;
}
// 4. 递归反序列化(可能并发执行)
LoadedIDMap oldToNewIDMap; // 旧ID → 新ID映射
bool success = false;
if (参数允许并发 && 文件足够大) {
// 使用QFuture异步加载(仅BIN格式支持)
QFuture<bool> future = QtConcurrent::run([&]() {
return root->fromFile(in, binVersion, flags, oldToNewIDMap);
});
// 显示进度(不阻塞)
while (!future.isFinished()) {
Sleep(500);
pDlg->setValue(pDlg->value() + 1);
QApplication::processEvents();
}
success = future.result();
} else {
// 同步加载
success = root->fromFile(in, binVersion, flags, oldToNewIDMap);
}
if (!success) {
delete root;
return CC_FERR_MALFORMED_FILE;
}
// 5. 重建对象间依赖关系
// 网格 → 顶点云, 折线 → 点云, 标签 → 点云/网格, 传感器 → 变换缓冲区
RepairDependencies(root, oldToNewIDMap);
// 6. 添加到容器
container.addChild(root);
return CC_FERR_NO_ERROR;
}
依赖关系修复
BIN格式的独特挑战:对象间存在引用(如网格引用顶点云)。解决方案:
cpp
// 查找依赖对象的鲁棒方法
ccHObject* FindRobust(
ccHObject* root, // 根对象
ccHObject* source, // 搜索起点
const LoadedIDMap& oldToNewIDMap, // ID映射表
unsigned oldUniqueID, // 旧ID
CC_CLASS_ENUM expectedType // 期望类型
) {
// 1. 在ID映射表中查找所有可能的新ID
auto it = oldToNewIDMap.find(oldUniqueID);
while (it != oldToNewIDMap.end() && it.key() == oldUniqueID) {
unsigned uniqueID = it.value();
++it;
// 2. 优先在source附近搜索(父节点和子节点)
if (source) {
ccHObject* parent = source->getParent();
if (Match(parent, uniqueID, expectedType))
return parent;
for (unsigned i = 0; i < source->getChildrenNumber(); ++i) {
ccHObject* child = source->getChild(i);
if (Match(child, uniqueID, expectedType))
return child;
}
}
// 3. 全树搜索(最后手段)
ccHObject* object = root->find(uniqueID);
if (object && object->isKindOf(expectedType))
return object;
}
return nullptr; // 未找到
}
// 使用示例:重建网格顶点引用
ccMesh* mesh = ...;
intptr_t cloudID = (intptr_t)mesh->getAssociatedCloud(); // 旧ID
ccHObject* cloud = FindRobust(root, mesh, oldToNewIDMap,
cloudID, CC_TYPES::POINT_CLOUD);
if (cloud) {
mesh->setAssociatedCloud(
ccHObjectCaster::ToGenericPointCloud(cloud), false);
} else {
ccLog::Error("Missing vertices for mesh!");
}
ccPointCloud序列化示例
cpp
bool ccPointCloud::toFile_MeOnly(QFile& out, short version) const {
// 1. 写入点数
uint32_t pointCount = size();
out.write((const char*)&pointCount, 4);
// 2. 写入坐标数组(连续内存)
out.write((const char*)m_points.data(), pointCount * sizeof(CCVector3));
// 3. 写入颜色数组(如果有)
bool hasColors = m_rgbaColors != nullptr;
out.write((const char*)&hasColors, 1);
if (hasColors) {
out.write((const char*)m_rgbaColors->data(),
pointCount * sizeof(ccColor::Rgba));
}
// 4. 写入法线索引数组(如果有)
bool hasNormals = m_normals != nullptr;
out.write((const char*)&hasNormals, 1);
if (hasNormals) {
out.write((const char*)m_normals->data(),
pointCount * sizeof(CompressedNormType));
}
// 5. 写入标量场数量
uint32_t sfCount = m_scalarFields.size();
out.write((const char*)&sfCount, 4);
for (ccScalarField* sf : m_scalarFields) {
sf->toFile(out, version); // 递归序列化
}
// 6. 写入扫描网格(如果有)
// 7. 写入全波形数据(如果有)
// ...
return true;
}
2.2 PLY格式 - Stanford PLY
文件位置 : CloudCompare/libs\qCC_io\src\PlyFilter.cpp (2121行)
第三方库:rply
CloudCompare使用rply库解析PLY文件(高效的流式解析器)。
PLY文件结构示例
ply
ply
format binary_little_endian 1.0
comment Created by CloudCompare
element vertex 1000000
property float x
property float y
property float z
property uchar red
property uchar green
property uchar blue
property float scalar_Intensity
property float nx
property float ny
property float nz
element face 500000
property list uchar int vertex_indices
end_header
<二进制数据>
元素结构识别
cpp
struct plyElement {
p_ply_element elem; // rply元素句柄
const char* elementName; // "vertex", "face"
long elementInstances; // 元素实例数
std::vector<plyProperty> properties; // 属性列表
bool isFace; // 是否为面
};
struct plyProperty {
p_ply_property prop; // rply属性句柄
const char* propName; // "x", "y", "z", "red"
e_ply_type type; // PLY_FLOAT, PLY_UCHAR
int elemIndex; // 属性索引
};
加载流程
cpp
CC_FILE_ERROR PlyFilter::loadFile(...) {
// 1. 使用rply打开文件
p_ply plyFile = ply_open(filename.toStdString().c_str(),
nullptr, 0, nullptr);
if (!plyFile) {
return CC_FERR_READING;
}
// 2. 读取头部
if (!ply_read_header(plyFile)) {
return CC_FERR_MALFORMED_FILE;
}
// 3. 遍历所有元素和属性,识别顶点、面、法线、颜色等
std::vector<plyElement> elements;
p_ply_element elem = ply_get_next_element(plyFile, nullptr);
while (elem) {
plyElement pElem;
pElem.elem = elem;
pElem.elementName = ply_get_element_name(elem);
pElem.elementInstances = ply_get_element_ninstances(elem);
pElem.isFace = (strcmp(pElem.elementName, "face") == 0);
// 遍历属性
p_ply_property prop = ply_get_next_property(elem, nullptr);
while (prop) {
plyProperty pProp;
pProp.prop = prop;
pProp.propName = ply_get_property_name(prop);
pProp.type = ply_get_property_type(prop);
pElem.properties.push_back(pProp);
prop = ply_get_next_property(elem, prop);
}
elements.push_back(pElem);
elem = ply_get_next_element(plyFile, elem);
}
// 4. 创建点云对象
ccPointCloud* cloud = new ccPointCloud("Cloud");
long vertexCount = GetVertexCount(elements);
cloud->reserve(vertexCount);
// 5. 注册回调函数处理每个属性值
cloudAttributesDescriptor cloudDesc;
cloudDesc.cloud = cloud;
cloudDesc.xCoordIndex = FindPropertyIndex(elements, "x");
cloudDesc.yCoordIndex = FindPropertyIndex(elements, "y");
cloudDesc.zCoordIndex = FindPropertyIndex(elements, "z");
// 顶点坐标回调
ply_set_read_cb(plyFile, "vertex", "x", vertex_cb, &cloudDesc, 0);
ply_set_read_cb(plyFile, "vertex", "y", vertex_cb, &cloudDesc, 1);
ply_set_read_cb(plyFile, "vertex", "z", vertex_cb, &cloudDesc, 2);
// 颜色回调(如果有)
if (HasProperty(elements, "red")) {
cloud->reserveTheRGBTable();
ply_set_read_cb(plyFile, "vertex", "red", color_cb, &cloudDesc, 0);
ply_set_read_cb(plyFile, "vertex", "green", color_cb, &cloudDesc, 1);
ply_set_read_cb(plyFile, "vertex", "blue", color_cb, &cloudDesc, 2);
}
// 法线回调(如果有)
// 标量场回调(识别 "scalar_" 前缀)
// ...
// 6. 开始读取数据(触发回调)
if (!ply_read(plyFile)) {
delete cloud;
return CC_FERR_MALFORMED_FILE;
}
// 7. 如果有面,创建网格
if (HasFaces(elements)) {
ccMesh* mesh = new ccMesh(cloud);
// 注册面回调
ply_set_read_cb(plyFile, "face", "vertex_indices",
face_cb, mesh, 0);
// ...
}
// 8. 清理并返回
ply_close(plyFile);
container.addChild(cloud);
return CC_FERR_NO_ERROR;
}
回调处理机制
cpp
// 顶点坐标回调
static int vertex_cb(p_ply_argument argument) {
void* pUserData;
long coordIndex;
ply_get_argument_user_data(argument, &pUserData, &coordIndex);
cloudAttributesDescriptor* cloudDesc =
static_cast<cloudAttributesDescriptor*>(pUserData);
double value = ply_get_argument_value(argument);
// 根据coordIndex确定是x/y/z
static CCVector3 P;
P.u[coordIndex] = static_cast<PointCoordinateType>(value);
// 当z坐标读取完毕时,添加点
if (coordIndex == 2) {
cloudDesc->cloud->addPoint(P);
}
return 1; // 继续
}
// 标量场检测(识别 "scalar_" 前缀)
if (strncmp(propName, "scalar_", 7) == 0) {
QString sfName = QString::fromUtf8(propName + 7);
int sfIndex = cloud->addScalarField(sfName.toStdString());
ply_set_read_cb(plyFile, "vertex", propName,
scalar_cb, &cloudDesc, sfIndex);
}
设计优势:
- 流式解析:内存占用小
- 回调驱动:灵活处理各种属性
- 自动识别:颜色、法线、标量场
2.3 ASCII格式 - 通用文本格式
文件位置 : CloudCompare/libs\qCC_io\src\AsciiFilter.cpp (1201行)
最灵活的格式
ASCII格式是CloudCompare中最灵活的格式,支持用户自定义列映射。
列类型定义
cpp
enum CC_ASCII_OPEN_DLG_TYPES {
ASCII_OPEN_DLG_X, Y, Z, // 坐标
ASCII_OPEN_DLG_NX, NY, NZ, // 法线
ASCII_OPEN_DLG_R, G, B, A, // 颜色(0-255)
ASCII_OPEN_DLG_Rf, Gf, Bf, Af, // 浮点颜色(0-1)
ASCII_OPEN_DLG_RGB32i, // 打包RGB (整数)
ASCII_OPEN_DLG_RGB32f, // 打包RGB (浮点)
ASCII_OPEN_DLG_Grey, // 灰度
ASCII_OPEN_DLG_Scalar, // 标量场
ASCII_OPEN_DLG_Label, // 标签
ASCII_OPEN_DLG_QuatW, QuatX, QuatY, QuatZ, // 四元数
ASCII_OPEN_DLG_Ignore // 忽略
};
AsciiOpenDlg对话框 - 用户交互核心
自动检测分隔符:
cpp
void AsciiOpenDlg::autoFindBestSeparator() {
QChar separators[] = {' ', '\t', ',', ';', '|'};
int bestConsistency = 0;
QChar bestSep = ' ';
for (QChar sep : separators) {
// 统计各行列数
std::map<int, int> columnCountFreq;
for (int i = 0; i < sampleLineCount; ++i) {
QString line = m_sampleLines[i];
QStringList parts = line.split(sep, Qt::SkipEmptyParts);
int columnCount = parts.size();
columnCountFreq[columnCount]++;
}
// 计算一致性(最频繁列数的比例)
int maxFreq = 0;
for (const auto& pair : columnCountFreq) {
maxFreq = std::max(maxFreq, pair.second);
}
int consistency = (maxFreq * 100) / sampleLineCount;
// 选择一致性最高的分隔符
if (consistency > bestConsistency) {
bestConsistency = consistency;
bestSep = sep;
}
}
m_separator = bestSep;
}
头部识别:
cpp
// 检测第一行是否为列名
bool AsciiOpenDlg::detectHeaderLine() {
QString firstLine = m_sampleLines[0];
QStringList parts = firstLine.split(m_separator, Qt::SkipEmptyParts);
// 检测是否包含典型列名
bool hasTypicalColumnNames = false;
for (const QString& part : parts) {
QString lower = part.toLower();
if (lower.contains("x") || lower.contains("y") || lower.contains("z") ||
lower.contains("red") || lower.contains("green") || lower.contains("blue")) {
hasTypicalColumnNames = true;
break;
}
}
// 检测是否所有列都是文本(非数字)
bool allText = true;
for (const QString& part : parts) {
bool ok;
part.toDouble(&ok);
if (ok) {
allText = false;
break;
}
}
return hasTypicalColumnNames && allText;
}
数值类型检测:
cpp
std::vector<ColumnType> AsciiOpenDlg::detectColumnTypes() {
std::vector<ColumnType> columnTypes;
for (int col = 0; col < m_columnCount; ++col) {
bool allValid = true;
for (int row = 0; row < sampleLineCount; ++row) {
QString value = GetValue(row, col);
bool ok;
value.toDouble(&ok);
if (!ok) {
allValid = false;
break;
}
}
columnTypes.push_back(allValid ? VALID : TEXT);
}
return columnTypes;
}
加载流程
cpp
CC_FILE_ERROR AsciiFilter::loadCloudFromFormatedAsciiStream(
QTextStream& stream,
const AsciiOpenDlg::Sequence& openSequence, // 列映射序列
char separator, // 分隔符
bool commaAsDecimal, // 逗号作为小数点
unsigned approximateNumberOfLines,
unsigned maxCloudSize,
unsigned skipLines, // 跳过的行数
...
) {
// 1. 准备点云和属性描述符
cloudAttributesDescriptor cloudDesc = prepareCloud(openSequence, cloudChunkSize, ...);
// 2. 设置区域(处理逗号作为小数点)
QLocale locale = commaAsDecimal ? QLocale::German : QLocale::C;
// 3. 跳过指定行数(例如跳过头部)
for (unsigned i = 0; i < skipLines; ++i) {
if (stream.atEnd()) {
return CC_FERR_MALFORMED_FILE;
}
stream.readLine();
}
// 4. 逐行解析
unsigned pointsRead = 0;
CCVector3d Pshift(0, 0, 0); // 全局偏移
while (!stream.atEnd()) {
QString currentLine = stream.readLine();
QStringList parts = currentLine.split(separator, Qt::SkipEmptyParts);
if (parts.size() < cloudDesc.xCoordIndex + 1) {
continue; // 跳过不完整的行
}
// 5. 根据openSequence映射提取数据
CCVector3 P;
P.x = locale.toDouble(parts[cloudDesc.xCoordIndex]);
P.y = locale.toDouble(parts[cloudDesc.yCoordIndex]);
P.z = locale.toDouble(parts[cloudDesc.zCoordIndex]);
// 6. 处理全局偏移(仅第一个点)
if (pointsRead == 0) {
if (FileIOFilter::HandleGlobalShift(CCVector3d(P.x, P.y, P.z),
Pshift, parameters, ...)) {
cloudDesc.cloud->setGlobalShift(Pshift);
}
}
// 7. 添加点(应用偏移)
cloudDesc.cloud->addPoint(P - CCVector3(Pshift));
// 8. 处理颜色
if (cloudDesc.hasRGBColor) {
ccColor::Rgba col;
col.r = parts[cloudDesc.rCoordIndex].toInt();
col.g = parts[cloudDesc.gCoordIndex].toInt();
col.b = parts[cloudDesc.bCoordIndex].toInt();
col.a = cloudDesc.hasAlpha ? parts[cloudDesc.aCoordIndex].toInt() : 255;
cloudDesc.cloud->addColor(col);
}
// 9. 处理法线
if (cloudDesc.hasNorms) {
CCVector3 N;
N.x = parts[cloudDesc.nxCoordIndex].toFloat();
N.y = parts[cloudDesc.nyCoordIndex].toFloat();
N.z = parts[cloudDesc.nzCoordIndex].toFloat();
cloudDesc.cloud->addNorm(N);
}
// 10. 处理标量场
for (int sfIndex : cloudDesc.scalarIndexes) {
ScalarType value = parts[sfIndex].toFloat();
cloudDesc.cloud->getScalarField(sfIndex)->addElement(value);
}
pointsRead++;
// 11. 分块处理(当点数超过限制时)
if (pointsRead >= cloudChunkSize) {
if (cloudChunkSize < maxCloudSize) {
// 扩大当前云
unsigned newSize = std::min(cloudChunkSize * 2, maxCloudSize);
cloudDesc.cloud->reserve(newSize);
cloudChunkSize = newSize;
} else {
// 创建新云
container.addChild(cloudDesc.cloud);
cloudDesc.cloud = new ccPointCloud("Cloud_chunk");
cloudDesc.cloud->reserve(cloudChunkSize);
pointsRead = 0;
}
}
}
// 12. 添加最后一个云
if (cloudDesc.cloud && cloudDesc.cloud->size() > 0) {
container.addChild(cloudDesc.cloud);
}
return CC_FERR_NO_ERROR;
}
分块策略:
- 当点云超过
CC_MAX_NUMBER_OF_POINTS_PER_CLOUD(32位:1.28亿, 64位:20亿) - 自动分割成多个子云
- 重新估算文件大小以优化内存分配
2.4 LAS格式 - LiDAR数据
插件位置 : CloudCompare/plugins\core\IO\qLASIO/
通过LASZIP库处理
cpp
// LASFilter: 基于LASZIP的LAS/LAZ读写
class LASFilter : public FileIOFilter {
// 支持LAS 1.0-1.4版本
// 支持LAZ压缩格式
// 自动识别点格式(0-10)
};
自动提取LiDAR属性
cpp
// 创建标量场存储LiDAR特有属性
cloud->addScalarField("Classification"); // 分类码
cloud->addScalarField("Intensity"); // 强度
cloud->addScalarField("Return Number"); // 返回编号
cloud->addScalarField("Number of Returns"); // 总返回数
cloud->addScalarField("Scan Direction"); // 扫描方向
cloud->addScalarField("Edge of Flight Line"); // 飞行线边缘
cloud->addScalarField("Scan Angle Rank"); // 扫描角度
cloud->addScalarField("User Data"); // 用户数据
cloud->addScalarField("Point Source ID"); // 点源ID
cloud->addScalarField("GPS Time"); // GPS时间
3. 点云数据加载流程
3.1 完整加载调用链
用户操作(拖放文件/菜单打开)
↓
MainWindow::addToDB(filenames)
↓
FileIOFilter::LoadFromFile(filename, parameters, result, fileFilter)
├─ GetFilter(fileFilter) 或 FindBestFilterForExtension(extension)
├─ IncreaseSesionCounter() // 会话计数(用于"Apply All")
└─ filter->loadFile(filename, *container, loadParameters)
↓
具体过滤器实现 (BinFilter/PlyFilter/AsciiFilter/LASFilter等)
↓
ccPointCloud::reserve/addPoint/addColor/addNorm/addScalarField...
↓
container.addChild(cloud)
↓
MainWindow::addToDB(newGroup, ...)
├─ 添加到DB树(ccDBRoot)
├─ 设置显示窗口
├─ 自动缩放视图(可选)
└─ 刷新3D视图
3.2 内存管理和优化
预分配策略
cpp
ccPointCloud* cloud = new ccPointCloud("Cloud");
// 1. 预分配点表(连续内存)
if (!cloud->reserveThePointsTable(pointCount)) {
ccLog::Error("Not enough memory for points");
delete cloud;
return CC_FERR_NOT_ENOUGH_MEMORY;
}
// 2. 预分配颜色表(可选)
if (hasColors) {
if (!cloud->reserveTheRGBTable()) {
ccLog::Warning("Not enough memory for colors");
// 继续,但不加载颜色
}
}
// 3. 预分配法线表(可选)
if (hasNormals) {
if (!cloud->reserveTheNormsTable()) {
ccLog::Warning("Not enough memory for normals");
// 继续,但不加载法线
}
}
// 4. 预分配标量场
for (int i = 0; i < scalarFieldCount; ++i) {
int sfIdx = cloud->addScalarField(scalarFieldName);
ccScalarField* sf = cloud->getScalarField(sfIdx);
if (!sf->reserve(pointCount)) {
ccLog::Warning(QString("Not enough memory for SF '%1'")
.arg(scalarFieldName));
// 删除该标量场
cloud->deleteScalarField(sfIdx);
}
}
优势:
- 避免频繁重分配(
std::vector::push_back的陷阱) - 连续内存,缓存友好
- 一次性内存检查
内存限制
cpp
// ccPointCloud.h
#if defined(CC_ENV_32)
// 32位系统:约1.5GB内存
const unsigned CC_MAX_NUMBER_OF_POINTS_PER_CLOUD = 128000000;
#else
// 64位系统:约24GB内存
const unsigned CC_MAX_NUMBER_OF_POINTS_PER_CLOUD = 2000000000;
#endif
计算公式:
内存占用(字节) = 点数 × (
12 (坐标,float)
+ 4 (颜色,可选)
+ 2 (法线索引,可选)
+ 4 × 标量场数量
)
分块加载(ASCII)
cpp
unsigned cloudChunkSize = std::min(maxCloudSize, approximateNumberOfLines);
unsigned pointsRead = 0;
while (!stream.atEnd()) {
// ... 读取点
pointsRead++;
// 检查是否达到块大小限制
if (pointsRead >= cloudChunkSize) {
if (cloudChunkSize < maxCloudSize) {
// 选项1: 扩大当前云(2倍增长)
unsigned newSize = std::min(cloudChunkSize * 2, maxCloudSize);
if (cloud->reserve(newSize)) {
cloudChunkSize = newSize;
} else {
// 内存不足,创建新云
goto CreateNewCloud;
}
} else {
CreateNewCloud:
// 选项2: 创建新云
container.addChild(cloud);
cloud = new ccPointCloud(QString("Cloud_chunk_%1").arg(chunkIndex++));
cloud->reserve(cloudChunkSize);
pointsRead = 0;
}
}
}
3.3 大文件处理策略
进度监控
cpp
QScopedPointer<ccProgressDialog> pDlg(nullptr);
if (parameters.parentWidget) {
pDlg.reset(new ccProgressDialog(true, parameters.parentWidget));
pDlg->setMethodTitle("Loading file");
pDlg->setInfo(QString("Points: %1").arg(pointCount));
pDlg->start();
}
// 使用CCCoreLib的归一化进度
CCCoreLib::NormalizedProgress nprogress(pDlg.data(), totalPoints);
for (unsigned i = 0; i < totalPoints; ++i) {
// 加载点...
// 更新进度(自动调整更新频率)
if (!nprogress.oneStep()) {
// 用户点击取消
return CC_FERR_CANCELED_BY_USER;
}
}
if (pDlg) {
pDlg->stop();
}
NormalizedProgress特性:
- 自动调整更新间隔(避免频繁刷新UI)
- 典型更新间隔: 每1%或每100ms
- 用户可随时取消
自适应估算(ASCII)
cpp
// 初始估算(基于文件大小)
QFileInfo fileInfo(filename);
qint64 fileSize = fileInfo.size();
double averageLineSize = 50.0; // 假设
unsigned approximateNumberOfLines =
static_cast<unsigned>(fileSize / averageLineSize);
// 读取过程中重新估算
unsigned pointsRead = 0;
qint64 charactersRead = stream.pos();
if (pointsRead > 1000) {
// 计算实际平均行大小
averageLineSize = static_cast<double>(charactersRead) /
(pointsRead + skipLines);
// 重新估算总行数
double newApproximation = fileSize / averageLineSize - skipLines;
approximateNumberOfLines = static_cast<unsigned>(ceil(newApproximation));
// 更新进度条
pDlg->setMaximum(approximateNumberOfLines);
}
3.4 多线程加载
BIN格式:真并发
cpp
// 使用Qt的并发框架
QFuture<bool> future = QtConcurrent::run([&]() {
return root->fromFile(in, binVersion, flags, oldToNewIDMap);
});
// 显示进度(主线程不阻塞)
while (!future.isFinished()) {
Sleep(500);
pDlg->setValue(pDlg->value() + 1);
QApplication::processEvents(); // 保持UI响应
}
bool success = future.result();
ASCII/PLY格式:伪并发
由于文件指针顺序读取限制,仅使用后台线程避免阻塞UI:
cpp
// 后台线程加载
bool loadingFinished = false;
std::thread loadThread([&]() {
// 加载逻辑...
loadingFinished = true;
});
// 主线程保持UI响应
while (!loadingFinished) {
Sleep(100);
QApplication::processEvents();
}
loadThread.join();
4. 数据结构详解
4.1 ccPointCloud类层次
继承层次:
ccSerializableObject (序列化接口)
↓
ccObject (ID、名称、元数据)
↓
ccDrawableObject (显示参数)
↓
ccShiftedObject (全局偏移)
↓
GenericIndexedCloudPersist (CCCoreLib接口)
↓
ccGenericPointCloud (通用点云接口)
↓
PointCloudTpl<ccGenericPointCloud, QString> (模板基类)
↓
ccPointCloud (完整实现)
4.2 核心成员变量
cpp
class ccPointCloud : public PointCloudTpl<ccGenericPointCloud, QString> {
protected:
// ===== 点数据(继承自PointCloudTpl) =====
std::vector<CCVector3> m_points; // 点坐标(float或double)
// ===== 颜色(可选) =====
RGBAColorsTableType* m_rgbaColors; // std::vector<ccColor::Rgba>*
// 4字节/点 (R, G, B, A各8位)
// ===== 法线(压缩存储,可选) =====
NormsIndexesTableType* m_normals; // std::vector<CompressedNormType>*
// 2字节/点 (16位索引,指向预计算的法向量表)
// ===== 标量场(可选多个) =====
std::vector<ccScalarField*> m_scalarFields;
ccScalarField* m_currentDisplayedScalarField;
int m_currentDisplayedScalarFieldIndex;
// 4字节/点/场
// ===== 扫描网格(结构化扫描数据) =====
std::vector<Grid::Shared> m_grids;
// ===== 全波形数据(LiDAR) =====
FWFDescriptorSet m_fwfDescriptors;
std::vector<ccWaveform> m_fwfWaveforms;
SharedFWFDataContainer m_fwfData;
// ===== VBO管理(GPU加速) =====
vboSet m_vboManager;
// ===== LOD系统 =====
ccPointCloudLOD* m_lod;
bool m_useLODRendering;
// ===== 全局偏移(继承自ccShiftedObject) =====
CCVector3d m_globalShift; // 偏移向量
double m_globalScale; // 缩放因子(通常为1.0)
};
4.3 点坐标存储
CCVector3结构(CCCoreLib):
cpp
template<typename T>
class Vector3Tpl {
union {
struct { T x, y, z; };
T u[3];
};
};
using CCVector3 = Vector3Tpl<PointCoordinateType>;
// PointCoordinateType = float 或 double (编译时配置)
内存布局:
点0: [x0][y0][z0]
点1: [x1][y1][z1]
点2: [x2][y2][z2]
...
大小:
- float版本: 12字节/点
- double版本: 24字节/点
4.4 颜色存储
ccColor::Rgba结构:
cpp
namespace ccColor {
struct Rgba {
union {
struct { ColorCompType r, g, b, a; };
ColorCompType rgba[4];
};
};
}
// ColorCompType = unsigned char (8位)
大小: 4字节/点
可选性检查:
cpp
bool ccPointCloud::hasColors() const {
return m_rgbaColors != nullptr &&
m_rgbaColors->size() >= size();
}
4.5 法线存储
压缩法线(节省6倍内存):
cpp
using CompressedNormType = unsigned short; // 16位
// 法线压缩表(ccNormalVectors.cpp)
static CCVector3 s_normalVectors[COMPRESSED_NORMAL_VECTORS_NUMBER];
// COMPRESSED_NORMAL_VECTORS_NUMBER = 65536 (2^16)
// 压缩: 3D向量 → 16位索引
CompressedNormType ccNormalVectors::CompressNormal(const CCVector3& N) {
// 在量化法线表中查找最接近的
// 使用球面哈希加速查找
return FindClosestIndex(N);
}
// 解压: 16位索引 → 3D向量
const CCVector3& ccNormalVectors::GetNormal(CompressedNormType index) {
assert(index < COMPRESSED_NORMAL_VECTORS_NUMBER);
return s_normalVectors[index];
}
优势:
- 内存: 2字节 vs 12字节 (6倍压缩)
- 缓存: 法线表仅64KB,常驻缓存
- 精度: 角度误差约1度(65536个方向)
4.6 标量场管理
ccScalarField类:
cpp
class ccScalarField : public CCCoreLib::ScalarField {
std::vector<ScalarType> m_values; // ScalarType = float (4字节)
std::string m_name; // 标量场名称
// 统计信息
ScalarType m_minVal, m_maxVal;
bool m_validMinMax;
// 显示参数
ccColorScale::Shared m_colorScale; // 色标
double m_displayRange[2]; // 显示范围
bool m_symmetricalScale; // 对称色标
bool m_logScale; // 对数色标
};
多标量场支持:
cpp
// 添加标量场
int sfIndex = cloud->addScalarField("Intensity");
ccScalarField* sf = cloud->getScalarField(sfIndex);
sf->reserve(cloud->size());
// 设置值
for (unsigned i = 0; i < cloud->size(); ++i) {
sf->setValue(i, intensityValue);
}
// 设置当前显示的标量场
cloud->setCurrentDisplayedScalarField(sfIndex);
cloud->showSF(true);
// 计算统计信息
sf->computeMinAndMax();
内存: 4字节/点/标量场
4.7 全局偏移处理
为什么需要全局偏移?
float精度问题:
- float有效数字: 约7位
- 大坐标值(如UTM: 500000m)会导致精度损失
- 示例:
500000.12 - 500000.11 = 0.0(精度不足)
解决方案:
- 存储相对于偏移点的坐标
- 使用double存储偏移向量
ccShiftedObject接口
cpp
class ccShiftedObject {
protected:
CCVector3d m_globalShift; // 偏移向量(double精度)
double m_globalScale; // 缩放因子(通常为1.0)
public:
// 设置偏移
void setGlobalShift(const CCVector3d& shift);
void setGlobalScale(double scale);
// 获取真实坐标
CCVector3d toGlobal3d(const CCVector3& localP) const {
return CCVector3d(localP) * m_globalScale + m_globalShift;
}
// 获取局部坐标
CCVector3 toLocal(const CCVector3d& globalP) const {
return CCVector3((globalP - m_globalShift) / m_globalScale);
}
// 检查
bool isShifted() const {
return m_globalShift.norm2() > 0 || m_globalScale != 1.0;
}
};
加载时处理流程
cpp
// 读取第一个点
CCVector3d firstPoint = ReadPointFromFile();
CCVector3d Pshift(0, 0, 0);
bool preserveCoordinateShift = true;
// 检查是否需要偏移
if (FileIOFilter::HandleGlobalShift(firstPoint, Pshift,
parameters,
preserveCoordinateShift)) {
// 应用偏移
cloud->setGlobalShift(Pshift);
// 存储相对坐标
for (each point P) {
CCVector3 localP = CCVector3(P - Pshift);
cloud->addPoint(localP);
}
}
保存时处理流程
cpp
if (cloud->isShifted() && preserveCoordinateShift) {
// 恢复全局坐标
for (unsigned i = 0; i < cloud->size(); ++i) {
const CCVector3* localP = cloud->getPoint(i);
CCVector3d globalP = cloud->toGlobal3d(*localP);
// 写入全局坐标
WritePointToFile(globalP);
}
} else {
// 直接写入局部坐标
for (unsigned i = 0; i < cloud->size(); ++i) {
const CCVector3* localP = cloud->getPoint(i);
WritePointToFile(*localP);
}
}
ccGlobalShiftManager策略
cpp
class ccGlobalShiftManager {
public:
enum Mode {
NO_DIALOG, // 不处理偏移
NO_DIALOG_AUTO_SHIFT, // 自动偏移(无对话框)
DIALOG_IF_NECESSARY, // 必要时显示对话框
ALWAYS_DISPLAY_DIALOG // 始终显示对话框
};
// 检查是否需要偏移
static bool NeedShift(const CCVector3d& P) {
return std::abs(P.x) > MAX_COORDINATE_ABS_VALUE // 默认10^8
|| std::abs(P.y) > MAX_COORDINATE_ABS_VALUE
|| std::abs(P.z) > MAX_COORDINATE_ABS_VALUE;
}
// 计算建议的偏移
static CCVector3d BestShift(const CCVector3d& P) {
// 四舍五入到最近的10^6
return CCVector3d(
std::round(P.x / 1e6) * 1e6,
std::round(P.y / 1e6) * 1e6,
std::round(P.z / 1e6) * 1e6
);
}
// 处理偏移(主函数)
static bool HandleGlobalShift(
const CCVector3d& P,
CCVector3d& Pshift,
LoadParameters& params,
bool& preserveShiftOnSave
);
};
5. GUI集成
5.1 文件打开对话框
MainWindow::doActionLoadFile:
cpp
void MainWindow::doActionLoadFile() {
// 1. 构建文件过滤器列表
QStringList filters = FileIOFilter::ImportFilterList();
// 结果示例:
// "All (*.*)"
// "CloudCompare entities (*.bin)"
// "ASCII cloud (*.txt *.asc *.xyz *.pts ...)"
// "PLY mesh (*.ply)"
// "LAS cloud (*.las *.laz)"
// ...
// 2. 显示文件选择对话框
QStringList filenames = QFileDialog::getOpenFileNames(
this,
tr("Open file(s)"),
m_currentPath,
filters.join(";;"),
nullptr,
QFileDialog::DontUseNativeDialog
);
if (filenames.isEmpty())
return;
// 3. 记住路径
m_currentPath = QFileInfo(filenames.first()).absolutePath();
// 4. 批量加载
addToDB(filenames);
}
5.2 进度显示
ccProgressDialog - 模态/非模态进度窗口:
cpp
// 创建进度对话框
QScopedPointer<ccProgressDialog> pDlg(nullptr);
if (parameters.parentWidget) {
pDlg.reset(new ccProgressDialog(
true, // 可取消
parameters.parentWidget
));
pDlg->setMethodTitle(tr("Loading file"));
pDlg->setInfo(QString("File: %1").arg(filename));
pDlg->setRange(0, pointCount);
pDlg->start();
}
// 使用CCCoreLib的归一化进度
CCCoreLib::NormalizedProgress nprogress(pDlg.data(), pointCount);
for (unsigned i = 0; i < pointCount; ++i) {
// 加载点...
// 更新进度
if (!nprogress.oneStep()) {
// 用户点击取消
delete cloud;
return CC_FERR_CANCELED_BY_USER;
}
}
if (pDlg) {
pDlg->stop();
}
自适应更新频率:
- NormalizedProgress自动调整更新间隔
- 避免频繁刷新UI导致性能下降
- 典型间隔: 每1%或每100ms
5.3 用户交互
ASCII加载对话框
AsciiOpenDlg特性:
- 文件预览: 显示前100行
- 自动检测: 分隔符、列类型
- 手动映射: 用户自定义列角色
- 配置保存: "Apply All"重用配置
界面元素:
cpp
QTableWidget* m_previewTable; // 预览表格
QComboBox* m_separatorCombo; // 分隔符选择
QCheckBox* m_skipLinesCheckBox; // 跳过行数
QSpinBox* m_skipLinesSpinBox;
// 列映射(每列一个ComboBox)
std::vector<QComboBox*> m_columnRoleCombo;
全局偏移对话框
ccShiftAndScaleCloudDlg:
cpp
// 显示建议偏移
QString message = QString(
"Coordinates are too large:\n"
"X: %1\n"
"Y: %2\n"
"Z: %3\n\n"
"Suggested shift:\n"
"(%4, %5, %6)\n\n"
"Apply shift?"
).arg(P.x, 0, 'f', 2)
.arg(P.y, 0, 'f', 2)
.arg(P.z, 0, 'f', 2)
.arg(Pshift.x, 0, 'f', 2)
.arg(Pshift.y, 0, 'f', 2)
.arg(Pshift.z, 0, 'f', 2);
// 用户选项:
// - Yes: 应用建议偏移
// - Edit: 手动编辑偏移
// - No: 不应用偏移
// - Yes to All: 应用到所有后续文件
错误消息
cpp
void FileIOFilter::DisplayErrorMessage(
CC_FILE_ERROR err,
const QString& action,
const QString& filename
) {
QString errorStr;
switch (err) {
case CC_FERR_NOT_ENOUGH_MEMORY:
errorStr = tr("not enough memory");
break;
case CC_FERR_MALFORMED_FILE:
errorStr = tr("malformed file");
break;
case CC_FERR_CANCELED_BY_USER:
return; // 不显示错误(用户主动取消)
// ... 其他错误
}
QString outputString = QString("An error occurred while %1 '%2': %3")
.arg(action)
.arg(QFileInfo(filename).fileName())
.arg(errorStr);
ccLog::Error(outputString);
if (QApplication::activeWindow()) {
QMessageBox::critical(QApplication::activeWindow(),
tr("I/O Error"),
outputString);
}
}
6. 关键函数详解
6.1 FileIOFilter::LoadFromFile
完整实现:
cpp
ccHObject* FileIOFilter::LoadFromFile(
const QString& filename,
LoadParameters& loadParameters,
CC_FILE_ERROR& result,
const QString& fileFilter // 可选:指定过滤器
) {
Shared filter;
// 1. 获取过滤器
if (!fileFilter.isEmpty()) {
// 用户指定了过滤器
filter = GetFilter(fileFilter, true);
} else {
// 根据扩展名自动选择
QString extension = QFileInfo(filename).suffix();
filter = FindBestFilterForExtension(extension);
}
if (!filter) {
ccLog::Warning(QString("[Load] Unknown file extension: %1")
.arg(extension));
result = CC_FERR_UNKNOWN_FILE;
return nullptr;
}
// 2. 检查文件存在性
if (!QFileInfo(filename).exists()) {
ccLog::Error(QString("[Load] File not found: %1").arg(filename));
result = CC_FERR_READING;
return nullptr;
}
// 3. 创建容器
ccHObject* container = new ccHObject();
// 4. 更新会话计数(用于"Apply All"功能)
unsigned sessionCounter = IncreaseSesionCounter();
loadParameters.sessionStart = (sessionCounter == 1);
// 5. 加载文件
try {
result = filter->loadFile(filename, *container, loadParameters);
}
catch (const std::exception& e) {
ccLog::Warning(QString("[I/O] Exception caught: %1").arg(e.what()));
result = CC_FERR_CONSOLE_ERROR;
}
catch (...) {
ccLog::Warning("[I/O] Unknown exception caught");
result = CC_FERR_CONSOLE_ERROR;
}
// 6. 后处理
if (result == CC_FERR_NO_ERROR) {
ccLog::Print(QString("[I/O] File '%1' loaded successfully")
.arg(filename));
// 设置容器名称
QFileInfo fi(filename);
container->setName(QString("%1 (%2)")
.arg(fi.fileName())
.arg(fi.absolutePath()));
// 重命名子对象(替换"unnamed"为文件名)
for (unsigned i = 0; i < container->getChildrenNumber(); ++i) {
ccHObject* child = container->getChild(i);
QString name = child->getName();
if (name.startsWith("unnamed")) {
name.replace("unnamed", fi.completeBaseName());
child->setName(name);
}
}
} else {
DisplayErrorMessage(result, "loading", filename);
// 如果容器为空,删除它
if (container->getChildrenNumber() == 0) {
delete container;
container = nullptr;
}
}
return container;
}
6.2 ccPointCloud::reserve
预分配所有相关数组:
cpp
bool ccPointCloud::reserve(unsigned numberOfPoints) {
// 1. 预留点表
if (!reserveThePointsTable(numberOfPoints)) {
ccLog::Error("Not enough memory for points");
return false;
}
// 2. 预留颜色表
if (m_rgbaColors) {
if (!m_rgbaColors->reserve(numberOfPoints)) {
ccLog::Warning("Not enough memory for colors");
unallocateColors(); // 删除颜色表
}
}
// 3. 预留法线表
if (m_normals) {
if (!m_normals->reserve(numberOfPoints)) {
ccLog::Warning("Not enough memory for normals");
unallocateNorms(); // 删除法线表
}
}
// 4. 预留所有标量场
for (ccScalarField* sf : m_scalarFields) {
if (!sf->reserve(numberOfPoints)) {
ccLog::Warning(QString("Not enough memory for SF '%1'")
.arg(sf->getName()));
// 注意:不删除标量场,仅警告
}
}
// 5. 预留FWF表(如果有)
if (!m_fwfWaveforms.empty()) {
if (!reserveTheFWFTable()) {
ccLog::Warning("Not enough memory for FWF data");
clearFWFData();
}
}
return true;
}
7. 类图和数据流
7.1 FileIOFilter继承体系
FileIOFilter (抽象基类)
├─ BinFilter # CloudCompare原生格式
├─ AsciiFilter # ASCII文本
├─ PlyFilter # Stanford PLY
├─ DxfFilter # AutoCAD DXF
├─ ShpFilter # ESRI Shapefile
├─ RasterGridFilter # GeoTIFF等
├─ ImageFileFilter # PNG/JPG/BMP等
├─ DepthMapFileFilter # 深度图
└─ [插件过滤器]
├─ LASFilter # LiDAR LAS/LAZ (qLASIO插件)
├─ E57Filter # E57点云 (qE57IO插件)
├─ FBXFilter # Autodesk FBX (qFBXIO插件)
├─ PTXFilter # Leica PTX
└─ ...
7.2 文件加载数据流
┌───────────────┐
│ 用户操作 │ (拖放/菜单)
└───────┬───────┘
↓
┌───────────────────────────┐
│ MainWindow::addToDB │
└───────┬───────────────────┘
↓
┌─────────────────────────────────────┐
│ FileIOFilter::LoadFromFile │
│ ├─ 扩展名检测 │
│ ├─ 选择过滤器 │
│ └─ 会话计数 │
└───────┬─────────────────────────────┘
↓
┌──────────────────────────────────────┐
│ 具体过滤器 (BIN/PLY/ASCII/LAS) │
│ ├─ 解析文件格式 │
│ ├─ 验证文件完整性 │
│ └─ 提取元数据 │
└───────┬──────────────────────────────┘
↓
┌──────────────────────────┐
│ 创建ccPointCloud对象 │
│ ├─ reserve(n) │
│ ├─ addPoint() │
│ ├─ addColor() │
│ ├─ addNorm() │
│ └─ addScalarField() │
└───────┬──────────────────┘
↓
┌──────────────────────────┐
│ 全局偏移处理 │
│ ccGlobalShiftManager │
└───────┬──────────────────┘
↓
┌──────────────────────────┐
│ 容器组装 │
│ ccHObject (可能包含多个云) │
└───────┬──────────────────┘
↓
┌──────────────────────────┐
│ MainWindow::addToDB │
│ ├─ 添加到DB树 │
│ ├─ 设置显示窗口 │
│ ├─ 自动缩放视图 │
│ └─ 刷新3D视图 │
└──────────────────────────┘
7.3 BIN文件序列化流程
【保存】
ccPointCloud
↓
toFile_MeOnly(QFile& out, short version)
├─ 写入点数 (uint32_t)
├─ 写入坐标数组 (float[3*n] 或 double[3*n])
├─ 写入颜色数组 (uchar[4*n], 可选)
├─ 写入法线数组 (ushort[n], 可选)
├─ 写入标量场数量 (uint32_t)
│ └─ 对每个标量场:
│ ├─ 写入名称 (QString)
│ ├─ 写入值数组 (float[n])
│ └─ 写入显示参数
├─ 写入扫描网格 (可选)
├─ 写入全波形数据 (可选)
└─ 写入全局偏移/缩放
【加载】
QFile& in
↓
fromFile_MeOnly(QFile& in, short version, flags, idMap)
├─ 读取点数
├─ reserve(点数)
├─ 读取坐标数组
├─ 读取颜色数组 (可选)
├─ 读取法线数组 (可选)
├─ 读取标量场数量
│ └─ 对每个标量场:
│ ├─ 创建ccScalarField
│ ├─ 读取名称
│ ├─ 读取值数组
│ └─ 读取显示参数
├─ 读取扫描网格 (可选)
├─ 读取全波形数据 (可选)
└─ 读取全局偏移/缩放
↓
ccPointCloud (完整恢复)
免责声明
本文档仅供技术学习和交流使用,内容基于作者对CloudCompare开源项目的个人理解和分析整理。
重要提示:
- 本文档不代表CloudCompare官方文档或权威说明
- 文档内容为个人学习整理,仅供参考
- CloudCompare是开源项目,遵循GPL v2+许可证,版权归原作者所有
- 实际使用CloudCompare时应参考官方文档:https://github.com/CloudCompare/CloudCompare
- 文档内容可能存在理解偏差或过时信息,欢迎指正
- 因使用本文档产生的任何后果由使用者自行承担