通俗总结 Octree(八叉树)

第一步:建树(一次性做好,后面不再切盒子!重点!)

  1. 先用一个大盒子,把你所有的点云全部包进去。
  2. 把这个盒子,X/Y/Z三个方向同时对半切一刀 → 切成8个小盒子。
  3. 挨个看这些小盒子:
    • 如果小盒子的边长 还大于你设定的resolution(最小格子边长):继续把这个盒子切成8份;
    • 如果盒子边长 == resolution:停止切分!这个就是最小叶子盒子,不再切。
  4. 每个最小叶子盒子,记住落在它内部所有点的编号。

✅ 建树完成之后:所有小盒子位置固定,不会再变,不会临时切割。

就像把一块豆腐一次性切成很多固定大小的小方块,切完就定型了。


第二步:搜索(三种搜索,完全不一样)

给定一个查询点 searchPoint

① voxelSearch【八叉树特有!】

逻辑:

  1. 看这个查询点落在哪一块已经切好的小豆腐块(叶子盒子)里面
  2. 直接拿出这一整块豆腐里面全部的点返回

⚠️关键点:

是固定方块里面的点 ,不是"离查询点距离小于resolution"!

方块边界在建树的时候就固定死了,不会跟着查询点移动。

👉形状:立方体盒子。

举例子:小盒子边长128。查询点在这个盒子靠右的位置,盒子左边很远的点,只要在这个盒子里,也会被拿出来。

② nearestKSearch(K近邻)

逻辑:找离查询点最近的K个点 。

八叉树只是用来加速查找 :优先检查查询点附近的盒子,不用遍历全部1000个点。

👉输出最近K个,和KDTree功能一样,只是底层加速结构不同。

③ radiusSearch(半径搜索)

逻辑:以查询点为球心,画一个圆球,取出圆球里面所有点 。

同样利用八叉树快速跳过大量完全不在球内的盒子,减少计算。

👉形状:球体,可以跨好几个小豆腐块。

一句话区分3个搜索

  1. voxelSearch:取【同一个固定小方块】里面所有点(方块判定)
  2. radiusSearch:取【查询点为中心的圆球】里面所有点(距离判定)
  3. nearestKSearch:找【距离最近K个点】

超级简短一句话整体概括

八叉树就是预先把整个点云空间一次性切成一堆固定大小的小立方体;搜索的时候,利用这些预先切好的盒子快速查找点,voxelSearch直接拿查询点所在盒子内全部点,另外两种是球形/最近点查找,八叉树只是用来加速遍历。

对比KDTree:KDTree不会切成固定方块,没有voxelSearch。

cpp 复制代码
#include <pcl/point_cloud.h>   //点云头文件
#include <pcl/octree/octree.h> //八叉树头文件

#include <iostream>
#include <vector>
#include <ctime>

int main(int argc, char **argv)
{
    srand((unsigned int)time(NULL)); //用系统时间初始化随机种子与 srand (time (NULL))的区别

    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);

    // 创建点云数据
    cloud->width = 1000;
    cloud->height = 1; //无序
    cloud->points.resize(cloud->width * cloud->height);

    for (size_t i = 0; i < cloud->points.size(); ++i) //随机循环产生点云的坐标值
    {
        cloud->points[i].x = 1024.0f * rand() / (RAND_MAX + 1.0f);
        cloud->points[i].y = 1024.0f * rand() / (RAND_MAX + 1.0f);
        cloud->points[i].z = 1024.0f * rand() / (RAND_MAX + 1.0f);
    }
    /****************************************************************************
   创建一个octree实例,用设置分辨率进行初始化,该octree用它的页节点存放点索引向量,
   分辨率参数描述最低一级octree的最小体素的尺寸,因此octree的深度是分辨率和点云空间维度
   的函数,如果知道点云的边界框,应该用defineBoundingbox方法把它分配给octree然后通过
   点云指针把所有点增加到ctree中。
   *****************************************************************************/
    float resolution = 128.0f;

    pcl::octree::OctreePointCloudSearch<pcl::PointXYZ> octree(resolution); //初始化Octree

    octree.setInputCloud(cloud);      //设置输入点云    这两句是最关键的建立PointCloud和octree之间的联系
    octree.addPointsFromInputCloud(); //构建octree

    pcl::PointXYZ searchPoint; //设置searchPoint

    searchPoint.x = 1024.0f * rand() / (RAND_MAX + 1.0f);
    searchPoint.y = 1024.0f * rand() / (RAND_MAX + 1.0f);
    searchPoint.z = 1024.0f * rand() / (RAND_MAX + 1.0f);

    /*************************************************************************************
  一旦PointCloud和octree联系一起,就能进行搜索操作,这里使用的是"体素近邻搜索",把查询点所在体素中
   其他点的索引作为查询结果返回,结果以点索引向量的形式保存,因此搜索点和搜索结果之间的距离取决于octree的分辨率参数
*****************************************************************************************/

    std::vector<int> pointIdxVec; //存储体素近邻搜索结果向量

    if (octree.voxelSearch(searchPoint, pointIdxVec)) //执行搜索,返回结果到pointIdxVec
    {
        std::cout << "Neighbors within voxel search at (" << searchPoint.x
                  << " " << searchPoint.y
                  << " " << searchPoint.z << ")"
                  << std::endl;

        for (size_t i = 0; i < pointIdxVec.size(); ++i) //打印结果点坐标
            std::cout << "    " << cloud->points[pointIdxVec[i]].x
                      << " " << cloud->points[pointIdxVec[i]].y
                      << " " << cloud->points[pointIdxVec[i]].z << std::endl;
    }

    /**********************************************************************************
  K 被设置为10 ,K近邻搜索  方法把搜索结果写到两个分开的向量,第一个pointIdxNKNSearch包含搜索结果
   (结果点的索引的向量)  第二个向量pointNKNSquaredDistance存储搜索点与近邻之间的距离的平方。
*************************************************************************************/

    //K 近邻搜索
    int K = 10;

    std::vector<int> pointIdxNKNSearch;         //结果点的索引的向量
    std::vector<float> pointNKNSquaredDistance; //搜索点与近邻之间的距离的平方

    std::cout << "K nearest neighbor search at (" << searchPoint.x
              << " " << searchPoint.y
              << " " << searchPoint.z
              << ") with K=" << K << std::endl;

    if (octree.nearestKSearch(searchPoint, K, pointIdxNKNSearch, pointNKNSquaredDistance) > 0)
    {
        for (size_t i = 0; i < pointIdxNKNSearch.size(); ++i)
            std::cout << "    " << cloud->points[pointIdxNKNSearch[i]].x
                      << " " << cloud->points[pointIdxNKNSearch[i]].y
                      << " " << cloud->points[pointIdxNKNSearch[i]].z
                      << " (squared distance: " << pointNKNSquaredDistance[i] << ")" << std::endl;
    }

    // 半径内近邻搜索

    std::vector<int> pointIdxRadiusSearch;
    std::vector<float> pointRadiusSquaredDistance;

    float radius = 256.0f * rand() / (RAND_MAX + 1.0f);

    std::cout << "Neighbors within radius search at (" << searchPoint.x
              << " " << searchPoint.y
              << " " << searchPoint.z
              << ") with radius=" << radius << std::endl;

    if (octree.radiusSearch(searchPoint, radius, pointIdxRadiusSearch, pointRadiusSquaredDistance) > 0)
    {
        for (size_t i = 0; i < pointIdxRadiusSearch.size(); ++i)
            std::cout << "    " << cloud->points[pointIdxRadiusSearch[i]].x
                      << " " << cloud->points[pointIdxRadiusSearch[i]].y
                      << " " << cloud->points[pointIdxRadiusSearch[i]].z
                      << " (squared distance: " << pointRadiusSquaredDistance[i] << ")" << std::endl;
    }
}
cpp 复制代码
/*
 * @Description: 点云压缩(读取本地PCD文件版本):https://www.cnblogs.com/li-yao7758258/p/6436117.html
 * @Author: HCQ
 * @Company(School): UCAS
 * @Email: 1756260160@qq.com
 * @Date: 2020-10-13 10:10:33
 * @LastEditTime: 2020-10-27 16:02:18
 * @FilePath: /pcl-learning/03octree/1point_cloud_compression/point_cloud_compression.cpp
 */

#include <pcl/point_cloud.h>                        // 点云类型
#include <pcl/point_types.h>                        // 点数据类型
#include <pcl/io/pcd_io.h>                          // PCD文件读写
#include <pcl/visualization/cloud_viewer.h>         // 点云可视化类

#include <pcl/compression/octree_pointcloud_compression.h> // 点云压缩类

#include <stdio.h>
#include <sstream>
#include <stdlib.h>
#include <string>
#include <iostream>

#ifdef WIN32
#include <windows.h>
#define sleep(x) Sleep((x)*1000)
#else
#include <unistd.h> // sleep()
#endif

int main(int argc, char **argv)
{
    // 读取点云文件:优先使用命令行传入的文件名,否则使用默认路径
    std::string pcd_file;
    if (argc >= 2)
    {
        pcd_file = argv[1];
    }
    else
    {
        pcd_file = "../../../data/table_scene_mug_stereo_textured.pcd";
    }

    // 创建点云对象,从本地PCD文件加载点云
    pcl::PointCloud<pcl::PointXYZRGB>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZRGB>());

    if (pcl::io::loadPCDFile<pcl::PointXYZRGB>(pcd_file, *cloud) < 0)
    {
        std::cerr << "Error loading point cloud " << pcd_file << std::endl;
        return -1;
    }
    std::cout << "Loaded " << cloud->points.size() << " points from " << pcd_file << std::endl;

    // 是否在标准输出上打印压缩结果统计信息
    bool showStatistics = false;

    // 压缩选项详情在: /io/include/pcl/compression/compression_profiles.h
    // 使用带颜色的在线快速压缩配置参数集
    pcl::io::compression_Profiles_e compressionProfile = pcl::io::MED_RES_ONLINE_COMPRESSION_WITH_COLOR;

    // 初始化压缩和解压缩对象:压缩对象需要设定压缩参数选项,解压缩按照数据源自行判断
    pcl::io::OctreePointCloudCompression<pcl::PointXYZRGB> *PointCloudEncoder =
        new pcl::io::OctreePointCloudCompression<pcl::PointXYZRGB>(compressionProfile, showStatistics);
    pcl::io::OctreePointCloudCompression<pcl::PointXYZRGB> *PointCloudDecoder =
        new pcl::io::OctreePointCloudCompression<pcl::PointXYZRGB>();

    // 存储压缩点云的字节流对象
    std::stringstream compressedData;
    // 存储解压缩后的输出点云
    pcl::PointCloud<pcl::PointXYZRGB>::Ptr cloudOut(new pcl::PointCloud<pcl::PointXYZRGB>());

    // 压缩点云
    PointCloudEncoder->encodePointCloud(cloud, compressedData);
    std::cout << "Compressed data size: " << compressedData.str().size() << " bytes" << std::endl;

    // 解压缩点云
    PointCloudDecoder->decodePointCloud(compressedData, cloudOut);
    std::cout << "Decompressed points: " << cloudOut->points.size() << std::endl;

    // 可视化解压缩后的点云
    pcl::visualization::CloudViewer viewer("Point Cloud Compression Example");
    viewer.showCloud(cloudOut);

    // 阻塞等待,直到关闭可视化窗口
    while (!viewer.wasStopped())
    {
        sleep(1);
    }

    // 释放压缩与解压缩实例
    delete (PointCloudEncoder);
    delete (PointCloudDecoder);

    return 0;
}

逐行讲解这份PCL八叉树点云压缩代码

整体流程:读取本地pcd点云 → 创建八叉树压缩编码器+解码器 → 编码成二进制字节流 → 从字节流解码还原点云 → 可视化解压后的点云

核心类:pcl::io::OctreePointCloudCompression,基于八叉树实现有损点云压缩

复制代码
#include <pcl/point_cloud.h>                        // 点云容器头文件,定义PointCloud模板类
#include <pcl/point_types.h>                        // 点类型定义,这里要用pcl::PointXYZRGB
#include <pcl/io/pcd_io.h>                          // PCD文件读写:loadPCDFile
#include <pcl/visualization/cloud_viewer.h>         // 点云可视化窗口 CloudViewer
#include <pcl/compression/octree_pointcloud_compression.h> // 八叉树点云压缩核心头文件
#include <stdio.h>
#include <sstream>                                   // stringstream:内存里的字节流,存放压缩后的二进制数据
#include <stdlib.h>
#include <string>
#include <iostream>
// 跨平台sleep兼容:Windows和Linux sleep函数不一样
#ifdef WIN32
#include <windows.h>
#define sleep(x) Sleep((x)*1000)    // windows Sleep单位ms,所以*1000
#else
#include <unistd.h> // linux下sleep单位是秒
#endif

int main(int argc, char **argv)
{
    // ========== 1. 读取PCD文件,支持命令行传参 ==========
    std::string pcd_file;
    if (argc >= 2)
    {
        // 命令行运行 ./xxx your.pcd,argv[1]就是pcd路径
        pcd_file = argv[1];
    }
    else
    {
        // 不传参数,使用默认pcd文件
        pcd_file = "../../../data/table_scene_mug_stereo_textured.pcd";
    }

    // 创建点云智能指针,PointXYZRGB 带xyz坐标+RGB颜色
    pcl::PointCloud<pcl::PointXYZRGB>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZRGB>());
    // 加载PCD到cloud;返回<0代表读取失败
    if (pcl::io::loadPCDFile<pcl::PointXYZRGB>(pcd_file, *cloud) < 0)
    {
        std::cerr << "Error loading point cloud " << pcd_file << std::endl;
        return -1;
    }
    std::cout << "Loaded " << cloud->points.size() << " points from " << pcd_file << std::endl;

    // ========== 2. 创建八叉树压缩编码器、解码器 ==========
    bool showStatistics = false; // 是否打印压缩详细统计信息(耗时、压缩率等)
    // 压缩预设配置,来自compression_profiles.h
    // MED_RES_ONLINE_COMPRESSION_WITH_COLOR:中等分辨率、支持RGB颜色、适合在线实时流
    pcl::io::compression_Profiles_e compressionProfile = pcl::io::MED_RES_ONLINE_COMPRESSION_WITH_COLOR;

    // 【编码器】:构造参数:压缩配置、是否输出统计
    pcl::io::OctreePointCloudCompression<pcl::PointXYZRGB> *PointCloudEncoder =
        new pcl::io::OctreePointCloudCompression<pcl::PointXYZRGB>(compressionProfile, showStatistics);

    // 【解码器】:无参构造,不需要配置profile,压缩流内部自带配置信息,自动解析
    pcl::io::OctreePointCloudCompression<pcl::PointXYZRGB> *PointCloudDecoder =
        new pcl::io::OctreePointCloudCompression<pcl::PointXYZRGB>();

✅ 关键点:编码器需要预设压缩等级;解码器不需要,压缩后的字节流里已经保存了八叉树分辨率、颜色配置等信息,解码器自动读取。

复制代码
    // ==========3. 执行压缩 encodePointCloud ==========
    std::stringstream compressedData; // 内存字节流,存放压缩后的二进制数据
    pcl::PointCloud<pcl::PointXYZRGB>::Ptr cloudOut(new pcl::PointCloud<pcl::PointXYZRGB>());

    // 核心压缩函数
    // 参数:原始点云,输出字节流
    PointCloudEncoder->encodePointCloud(cloud, compressedData);
    std::cout << "Compressed data size: " << compressedData.str().size() << " bytes" << std::endl;

encodePointCloud内部做的事情:

  1. 根据profile的分辨率构建八叉树
  2. 体素合并:同一个体素内多个点合并成1个体素中心点(有损)
  3. 对xyz坐标、RGB颜色做量化编码
  4. 把编码后的二进制数据写入compressedData(内存流)

👉 这个compressedData.str()拿到std::string二进制,可以直接通过网络socket发送,或者写入本地二进制文件保存

复制代码
    // ==========4. 执行解压 decodePointCloud ==========
    PointCloudDecoder->decodePointCloud(compressedData, cloudOut);
    std::cout << "Decompressed points: " << cloudOut->points.size() << std::endl;

decodePointCloud:读取字节流,重建八叉树,还原点云到cloudOut

⚠️ 重点:cloudOut的点数 ≠ 原始cloud点数,坐标是近似值,有损压缩!

复制代码
    // ==========5. 可视化 ==========
    pcl::visualization::CloudViewer viewer("Point Cloud Compression Example");
    viewer.showCloud(cloudOut); // 显示解压后的点云

    // 阻塞循环:一直等待,直到关闭可视化窗口
    while (!viewer.wasStopped())
    {
        sleep(1);
    }

    // ==========6. 手动释放堆内存 ==========
    delete (PointCloudEncoder);
    delete (PointCloudDecoder);
    return 0;
}

这里用new创建对象,程序结束前必须delete释放;现代C++推荐用智能指针unique_ptr代替手动new/delete,避免内存泄漏。


整体逻辑简图

复制代码
PCD文件 → loadPCDFile → 原始点云cloud
                ↓
        Octree编码器 encode → 二进制字节流compressedData
                ↓
        Octree解码器 decode → 解压点云cloudOut
                ↓
        CloudViewer可视化 cloudOut

重要补充要点

  1. 压缩本质:八叉树体素量化
    不是对PCD文本做zip压缩。建树,设定体素大小,格子里多个点合并为中心点,所以是有损。

  2. 压缩预设选项(你可以替换)

    LOW_RES_ONLINE_COMPRESSION_WITHOUT_COLOR // 低分辨率、无颜色,压缩最强,失真大
    MED_RES_ONLINE_COMPRESSION_WITH_COLOR // 当前代码,中等分辨率带RGB
    HIGH_RES_ONLINE_COMPRESSION_WITH_COLOR // 高分辨率带RGB,失真最小,压缩包更大

  3. 典型用途
    机器人/深度相机实时点云网络传输;连续帧点云存储。
    ❌ 不适合高精度工业测量,坐标有误差。

可扩展小修改(可选)

  • 把compressedData.str()写入本地 .bin 文件,存压缩后的点云;
  • 读取bin文件,送入解码器,实现离线存储加载;
  • 打开showStatistics=true,打印压缩耗时、原始大小、压缩倍率。
相关推荐
星云低代码开发平台1 小时前
AI 连接器运行健康度:指标、异常恢复和联调清单
大数据·运维·网络·数据库·人工智能·mcp
2302_805771071 小时前
Day23:POM设计模式(封装)
java·开发语言·设计模式
SL_staff1 小时前
APS降本增效的关键:在JVS中实现‘人-机-料-法-环’全要素耦合调度
java·github·全栈
十年Java程序媛1 小时前
Java String、StringBuilder、StringBuffer 底层原理|字符串拼接性能与线程安全
java·开发语言·spring boot·安全·面试
鱼宵2 小时前
LangChain4j 生产化改造:记忆落 Redis、向量落 ES,重启再也不丢
数据库·redis·elasticsearch·langchain4j
SamDeepThinking2 小时前
CyclicBarrier的底层实现
java·后端·面试
我是人✓2 小时前
SpringMVC异常处理与拦截器实战揭秘
java·开发语言·后端
阿狗童鞋2 小时前
SpringSecurity权限认证实战指南
java·后端·spring
Wang's Blog2 小时前
Java 项目部署之 Docker工具快速入门: 学习路径与命令速查导引
java·学习·docker