02-cloudcompare点云加载与解析

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特性:

  1. 文件预览: 显示前100行
  2. 自动检测: 分隔符、列类型
  3. 手动映射: 用户自定义列角色
  4. 配置保存: "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
  • 文档内容可能存在理解偏差或过时信息,欢迎指正
  • 因使用本文档产生的任何后果由使用者自行承担
相关推荐
青铜弟弟4 个月前
倾斜摄影-点云数据处理一
倾斜摄影·点云数据处理·cloudcompare
想去看海9851 年前
CloudCompare下载、安装与汉化
cloudcompare