IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    三维点云处理入门(9):PCL点云数据知识(点云文件读写、Kd-Tree、OcTree)及其可视化

    52txr发表于 2024-03-19 17:01:00
    love 0

    点云数据是三维空间中大量点的集合,通常用于表示现实世界中的物体表面。在计算机视觉和机器人领域,点云数据被广泛应用于物体识别、位姿估计、3D建模等任务。在处理点云数据时,常用的格式有PCD、PLY、STL、OBJ和VTK等。其中,PCD是PCL最推荐和支持的格式,因为它可以存储更丰富的点云信息,并且读写效率较高。其他格式则需要转换或者额外的解析过程。除了基本的读写操作,PCL还提供了许多高级功能,如点云滤波、特征提取、配准、分割等。这些功能可以帮助我们更好地理解和利用点云数据。

    PCL支持的格式

    PCL支持多种点云格式,这些格式中,PCD是PCL最为优化和推荐的格式,因为它可以存储更丰富的点云信息,并且读写效率较高。其他格式则可能需要转换或者额外的解析过程。

    1. PCD(Point Cloud Data)格式:这是PCL的本地文件格式,用于存储点云数据。PCD格式支持存储点的位置、颜色、法线、曲率等信息。
    2. PLY(Polygon File Format)格式:这是一种广泛使用的文件格式,用于存储三维数据。PLY格式可以存储点云数据,也可以存储多边形网格。
    3. STL(Stereolithography)格式:这是用于三维打印和计算机辅助设计的文件格式,PCL可以从STL文件中导入点云数据。
    4. OBJ(Wavefront)格式:这是一个标准的3D模型格式,常用于存储三维几何信息。PCL可以解析OBJ格式文件中的点云数据。
    5. VTK(Visualization Toolkit)格式:VTK是一个开放源代码的软件系统,用于3D计算机图形学、图像处理以及可视化。PCL可以读取和写入VTK格式的点云数据。
    6. 3D文件格式:如3DS, COLLADA(.dae),FBX, Blender(.blend)等格式。PCL通过与其他库(如VTK)的集成,可以间接支持这些格式。
    7. ASCII文件:简单的文本文件,其中包含了点云的XYZ坐标,有时还包括颜色和法线信息。

    PCL(点云库)也支持从TXT文件格式中读取点云数据。通常,TXT格式的点云文件是以纯文本的形式存储的,其中包含了点云的坐标数据,有时还可能包括颜色信息、法线信息等其他属性。

    要在PCL中读取TXT格式的点云文件,通常需要编写一些额外的代码来解析文件内容并将其加载到PCL的数据结构中。PCL本身并没有直接的函数来读取TXT文件,因为TXT文件的格式不如PCD或其他专门的3D数据格式那样标准化。因此,用户需要根据TXT文件的具体格式编写相应的解析代码。

    PCD文件解析

    之前安装PCL的时候使用了一个兔子模型,事实上是可以用记事本打开的。

    PCD文件

    1. VERSION 0.7:指明了PCD文件的版本,这里是0.7版本。
    2. FIELDS x y z:列出了点云数据中每个点的字段。这里每个点有三个字段:x, y, z,分别代表点在三维空间中的坐标。
    3. SIZE 4 4 4:表示每个字段的大小(以字节为单位)。这里每个字段(x, y, z)都是4个字节。
    4. TYPE F F F:指出每个字段的数据类型。F 代表浮点数(float)。因此,x, y, z均为浮点数。
    5. COUNT 1 1 1:每个字段包含的元素数。这里每个字段(x, y, z)只包含一个元素。
    6. WIDTH 35947:点云的宽度。在这里,它表示点云中点的总数。
    7. HEIGHT 1:点云的高度。当高度为1时,这通常表示点云是无组织的(即不是以网格形式存储)。
    8. VIEWPOINT 0 0 0 1 0 0 0:定义了用于获取点云数据的观察点。这里的值0 0 0 1 0 0 0可能代表默认视角,其中前三个数字是位置,后四个数字是方向(用四元数表示)。
    9. POINTS 35947:点云中点的数量,这与WIDTH字段的值相同。
    10. DATA ascii:指定了点云数据的格式。在这种情况下,它是以ASCII文本形式存储的。

    随后的行列出了点云中每个点的实际坐标数据。每行表示一个点,其中包含三个浮点数,分别对应x, y, z坐标。

    PCD文件的读写

    PCD文件的读写

    这段代码是使用点云库(Point Cloud Library,PCL)来处理PCD(点云数据)文件的C++示例。

    代码包括两部分:读取PCD文件和写入PCD文件。

    #include <iostream> // 包含标准输入输出流
    #include <pcl/io/pcd_io.h> // PCL的PCD输入输出操作相关的头文件
    #include <pcl/point_types.h> // PCL的点类型定义头文件
    
    
    typedef pcl::PointXYZ PointT; // 起别名,命名PointT为pcl::PointXYZ类型,代表包含XYZ坐标的点
    typedef pcl::PointCloud<PointT> PointCloud; // 起别名,命名PointCloud为包含PointT类型点的点云数据结构
    PointCloud::Ptr cloud(new PointCloud); // 创建PointCloud类型的智能指针,指向新的点云对象

    读取PCD文件

    // 方法一
    pcl::PCDReader reader; // 创建PCDReader对象用于读取PCD文件
    reader.read("xxx.pcd", *cloud); // 使用reader对象的read方法,将"xxx.pcd"文件的内容读入到cloud指向的点云对象中
    
    // 方法二
    pcl::io::loadPCDFile("xxx.pcd", *cloud); // 直接使用pcl::io命名空间下的loadPCDFile函数读取文件到点云对象

    这两种方法实现了相同的功能,都是将"xxx.pcd"中的点云数据读入到cloud变量所指向的点云对象中。

    写入PCD文件

    // 方法一
    pcl::PCDWriter writer; // 创建PCDWriter对象用于写入PCD文件
    writer.write("xxx.pcd", *cloud); // 以默认(ASCII)格式写入点云数据到"xxx.pcd"文件
    writer.writeBinary("xxx.pcd", *cloud); // 以二进制格式写入点云数据到"xxx.pcd"文件
    writer.writeASCII("xxx.pcd", *cloud); // 以ASCII格式写入点云数据到"xxx.pcd"文件
    writer.writeBinaryCompressed("xxx.pcd", *cloud); // 以压缩的二进制格式写入点云数据到"xxx.pcd"文件
    
    // 方法二
    pcl::io::savePCDFile("xxx.pcd", *cloud); // 保存点云数据到"xxx.pcd"文件
    pcl::io::savePCDFileASCII("xxx.pcd", *cloud); // 以ASCII格式保存点云数据到"xxx.pcd"文件
    pcl::io::savePCDFileBinary("xxx.pcd", *cloud); // 以二进制格式保存点云数据到"xxx.pcd"文件
    pcl::io::savePCDFileBinaryCompressed("xxx.pcd", *cloud); // 以压缩的二进制格式保存点云数据到"xxx.pcd"文件

    写入PCD文件也有两种方法,分别是使用PCDWriter类和直接调用pcl::io命名空间下的函数。这些函数支持不同的数据格式,包括ASCII文本格式、二进制格式和压缩的二进制格式。这些格式提供了不同的存储和读写性能。

    PLY文件读写

    与PCD文件的读写也是类似的,这里就不做过多赘述。

    // 1. 读取
    // 方法一
    pcl::PLYReader reader;
    reader.read("xxx.ply", *cloud);
    // 方法二
    pcl::io::loadPLYFile("xxx.ply", *cloud);
    // 2. 保存
    // 方法一
    pcl::PLYWriter writer;
    writer.write("xxx.ply", *cloud);
    writer.writeBinary("xxx.ply", *cloud);
    writer.writeASCII("xxx.ply", *cloud);
    // 方法二
    pcl::io::savePLYFile("xxx.ply", *cloud); 
    pcl::io::savePLYFileASCII("xxx.ply", *cloud); 
    pcl::io::savePLYFileBinary("xxx.ply", *cloud);

    点云合并

    pcl::PointCloud<pcl::PointXYZ> cloud_a, cloud_b, cloud_c;
    pcl::PointCloud<pcl::Normal> n_cloud_b;
    pcl::PointCloud<pcl::PointNormal> p_n_cloud_c;

    声明了三个点云变量cloud_a, cloud_b, cloud_c,它们都是包含XYZ坐标的点云。同时,还声明了一个法线点云n_cloud_b,包含点的法线信息,以及一个pcl::PointNormal类型的点云p_n_cloud_c,它同时包含点的位置和法线信息。

    复制点云:

    //(1)复制点云
    cloud_c = cloud_a;
    cloud_c += cloud_b; // 拼接点云

    这部分代码展示了如何将一个点云赋值给另一个,以及如何将两个点云合并。cloud_c = cloud_a;是一个深拷贝操作,把cloud_a的内容完整复制到cloud_c。cloud_c += cloud_b;这一行将cloud_b点云的数据追加到cloud_c点云的末尾,实现了点云的合并。

    拼接点云:

    //(2)拼接点云
    pcl::concatenateFields(cloud_a, n_cloud_b, p_n_cloud_c); // 拼接字段,a和b的点数必须相同

    pcl::concatenateFields()函数用于将两个结构不同的点云合并为一个点云。cloud_a含有位置信息,n_cloud_b含有法线信息,此函数会把它们合并到p_n_cloud_c中,结果点云包含了位置和法线信息。注意,这种拼接要求cloud_a和n_cloud_b中的点数必须相同,每个位置对应的法线信息将合并到同一个点中。

    PCL的数据结构

    (1)Kd-Tree

    KD树讲解 - Bilibili

    KD-Tree原理详解 - 知乎

    详解PCL/FLANN中的KDTree算法 - 知乎

    Kd-Tree(K-dimensional tree,K维树)是一种用于组织点在K维空间中的数据结构,它可以让我们快速地进行空间搜索,比如查找最近的点(最近邻搜索)。

    很多的点分布在一个二维的平面上,如果你想找到离某个给定点最近的点,你可能需要查看平面上的每一个点,这非常耗时。但如果这些点按照某种规则被组织起来,就可以更快地找到你想要的点。

    Kd-Tree通过一系列的“切割”将空间分割成一系列的小部分,每次切割都是在一个维度上选择一个点作为“切点”将所有的点分成两组。在二维空间中,这个切割可以是一条线,所有在这条线左边的点归为一组,在右边的点归为另一组。然后,在这两组中的每一组内部,再选择另一个维度进行切割,如此递归下去。

    用树的结构来表示,每个“切点”都是一个节点,它有两个子节点,一个代表所有在切点一侧的点集,另一个代表另一侧的点集。最后,这个树的每个叶子节点会包含空间中的一个点或一些非常接近的几个点。

    当进行最近邻搜索时,我们可以从树的顶部开始,根据目标点的坐标选择向左还是向右下降,这样不必检查所有的点,就可以快速定位到最近的点或者最近的几个点。

    在三维空间(比如点云数据)中,Kd-Tree的原理是一样的,只是切割的是平面,而不是线。PCL库使用Kd-Tree来加速各种空间搜索操作,如最近邻搜索、范围搜索等。这在点云处理中非常重要,因为它可以大幅度提高处理效率。

    struct kdtree{
        Node-data - 数据矢量   数据集中某个数据点,是n维矢量(这里也就是k维)
        Range     - 空间矢量   该节点所代表的空间范围
        split     - 整数       垂直于分割超平面的方向轴序号
        Left      - kd树       由位于该节点分割超平面左子空间内所有数据点所构成的k-d树
        Right     - kd树       由位于该节点分割超平面右子空间内所有数据点所构成的k-d树
        parent    - kd树       父节点  
    }

    下面使用一个代码示例:

    #include <pcl/point_cloud.h>
    #include <pcl/kdtree/kdtree_flann.h>
    #include <iostream>
    #include <vector>
    
    // 创建一个KdTree对象用于空间搜索
    pcl::KdTreeFLANN<pcl::PointXYZ> kdtree;
    
    // 需要有一个已经填充了数据的点云对象 cloud
    // 这里假设 cloud 已经在某处被定义和填充了数据
    kdtree.setInputCloud (cloud);
    
    // 设置一个需要搜索的点 searchPoint
    pcl::PointXYZ searchPoint;
    // 这里应该为 searchPoint 赋值,假设它已经被设置了
    
    // K近邻搜索
    int K = 10;
    std::vector<int> pointIdxNKNSearch(K);
    std::vector<float> pointNKNSquaredDistance(K);
    
    // 执行搜索,返回找到的点的数量
    int num = kdtree.nearestKSearch (searchPoint, K, pointIdxNKNSearch, pointNKNSquaredDistance);
    
    // 半径搜索
    std::vector<int> pointIdxRadiusSearch;
    std::vector<float> pointRadiusSquaredDistance;
    float radius = ...; // 这里应该为 radius 赋值,表示搜索半径
    
    // 执行搜索,返回找到的点的数量
    int num = kdtree.radiusSearch (searchPoint, radius, pointIdxRadiusSearch, pointRadiusSquaredDistance);

    (2)OcTree

    八叉树结构通过对三维空间的几何实体进行体元剖分,每个体元具有相同的时间和空间复杂度,通过循环递归的划分方法对大小为2n*2n*2n的三维空间的几何对象进行剖分,从而构成一个具有根节点的方向图。在八叉树结构中如果被划分的体元具有相同的属性,则该体元构成一个叶节点;否则继续对该体元剖分成8个子立方体,对于2n*2n*2n大小的空间。

    简单来说,八叉树的空间划分方式是,把一个立方体分割为八个小立法体,然后递归地分割小立方体。

    八叉树应用案例:

    • 点云压缩
    • 空间划分和搜索
    • 无序点云数据的空间变化检测

    OcTree

    #include <pcl/point_cloud.h>
    #include <pcl/octree/octree.h>
    
    /*
    代码中的问题:
    searchPoint需要赋予具体坐标。
    在进行半径搜索之前,需要定义并初始化radius变量。
    在执行搜索之前,确保点云cloud已经被正确填充了数据。
    */
    
    //这行代码创建了一个点云对象。pcl::PointXYZ是一种常用的点云格式,其中包含X、Y和Z三个坐标。
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
    
    //设置Octree的分辨率
    //分辨率决定了Octree的体素(最小数据单元)大小。较小的分辨率会产生更精细的空间划分。
    float resolution = 128.0f;
    
    //创建并初始化Octree。创建了一个用于搜索的Octree,并将之前创建的点云设置为输入
    pcl::octree::OctreePointCloudSearch<pcl::PointXYZ> octree(resolution);
    octree.setInputCloud(cloud);
    octree.addPointsFromInputCloud();
    
    //定义搜索点
    //没有给出具体坐标,根据实际情况设置这个点的坐标。
    pcl::PointXYZ searchPoint;
    
    // 体素内近邻搜索
    //这段代码搜索与searchPoint在同一个体素内的所有点。结果存储在pointIdxVec中。
    std::vector<int> pointIdxVec;
    octree.voxelSearch(searchPoint, pointIdxVec);
    
    //K近邻搜索
    //进行K近邻搜索,寻找最接近searchPoint的K个点。结果包括点的索引和到搜索点的距离。
    int K = 10;
    std::vector<int> pointIdxNKNSearch;
    std::vector<float> pointNKNSquaredDistance;
    int num = octree.nearestKSearch(searchPoint, K, pointIdxNKNSearch, pointNKNSquaredDistance);
    
    //半径内近邻搜索
    //寻找在指定半径内的所有点。这里需要定义radius变量的值。
    std::vector<int> pointIdxRadiusSearch;
    std::vector<float> pointRadiusSquaredDistance;
    int num = octree.radiusSearch(searchPoint, radius, pointIdxRadiusSearch, pointRadiusSquaredDistance);

    OcTree

    点云可视化

    PCL中pcl_visualization库中提供了可视化相关的数据结构和组件,主要是可视化展示点云处理结果。其依赖于pcl_common、pcl_range_image、pcl_kdtree、pcl_IO以及VTK库。

    (1)简单的可视化

    pcl::visualization::CloudViewer viewer("Cloud Viewer"); //创建viewer对象
    viewer.showCloud(cloud);

    (2)复杂的可视化

    //创建PCLVisualizer对象
    pcl::visualization::PCLVisualizer viewer("3D Viewer");
    
    //添加点云到可视化器
    viewer.addPointCloud<pcl::PointXYZ>(cloud, "sample_cloud");
    
    //设置背景颜色
    viewer.setBackgroundColor(100, 100, 100);
    
    //添加第二个点云
    viewer.addPointCloud(cloud1, "cloud1");
    
    //设置点云颜色
    //这段代码为名为"cloud2"的点云设置了颜色处理器,颜色为红色(RGB值为255,0,0)
    pcl::visualization::PointCloudColorHandlerCustom<PointT> red(cloud2, 255, 0, 0);
    viewer.addPointCloud(cloud2, red, "cloud2");
    
    //添加多边形网格
    //添加一个多边形网格到可视化器,mesh是多边形网格对象
    viewer.addPolygonMesh(mesh);
    
    
    //在屏幕上添加文本
    //在可视化器的屏幕上的指定位置添加文本
    viewer.addText(string, position.x of screen, position.y of screen, ID=default("Text"), viewportID = default0);
    
    //在可视化器线程上运行函数
    viewer.runOnVisualizationThreadOnce(sub/function);
    viewer.runOnVisualizationThread(sub/function);
    //这些函数允许用户定义一个子函数(sub/function),该子函数可以一次性(runOnVisualizationThreadOnce)或连续地(runOnVisualizationThread)在可视化线程中运行。
    
    //旋转视图
    //这个命令使得可视化器处于一个等待用户输入的状态,同时持续刷新视图
    viewer.spin();
    
    //非阻塞旋转视图
    while (!viewer.wasStopped())
    {
      viewer.spinOnce(100);
      boost::this_thread::sleep(boost::posix_time::microseconds(10000));
    }
    //这个循环结构是一个常见的模式,用于在可视化窗口运行时更新视图但不阻塞程序的其他部分。viewer.spinOnce(100)会更新视图一次,并且等待100毫秒。boost::this_thread::sleep函数则会使当前线程暂停一段时间(这里是10毫秒),以减少CPU的负荷。
    
    //添加点云法线
    viewer.addPointCloudNormals<pcl::PointXYZRGB, pcl::Normal>(cloud, normals, 10, 0.05, "normals");
    //这行代码用于在可视化窗口中添加点云的法线。它显示每10个点的一个法线,每个法线的长度为0.05,并且将这组法线用"normals"标记。
    
    //添加几何图形
    // Line
    viewer.addLine<pcl::PointXYZRGB>(cloud->points[0], cloud->points[cloud->size() - 1], "line");
    // Sphere
    viewer.addSphere(cloud->points[0], 0.2, 0.5, 0.5, 0.0, "sphere");
    // Plane
    pcl::ModelCoefficients coeffs;
    coeffs.values.push_back(0.0);
    coeffs.values.push_back(0.0);
    coeffs.values.push_back(1.0);
    coeffs.values.push_back(0.0);
    viewer.addPlane(coeffs, "plane");
    //addLine 添加一条从点云的第一个点到最后一个点的直线。
    //addSphere 添加一个以点云的第一个点为中心,半径为0.2的球体,球体颜色为浅蓝色(RGB值为0.5, 0.5, 0.0)。
    //addPlane 添加一个平面,其系数由coeffs指定(这里定义了一个XY平面)。
    
    //创建两个分隔的视图(视窗)
    // 创建第一个视窗
    int v1(0);
    viewer.createViewPort(0, 0, 0.5, 1, v1);
    viewer.setBackgroundColor(0, 0, 0, v1);
    viewer.addText("TEXT 文字", 10, 10, "v1text", v1);
    // 创建第二个视窗
    int v2(0);
    viewer.createViewPort(0.5, 0, 1.0, 1.0, v2);
    viewer.setBackgroundColor(0.3, 0.3, 0.3, v2);
    viewer.addText("Radius: 0.1", 10, 10, "v2 text", v2);
    
    //键盘和鼠标的响应
    //这两行代码注册了键盘和鼠标的回调函数,这样当用户在可视化窗口中按键或者点击鼠标时,相应的函数将会被调用。
    viewer->registerKeyboardCallback(keyboardEventOccurred, (void*)&viewer);
    viewer->registerMouseCallback(mouseEventOccurred, (void*)&viewer);
    //这些是回调函数的原型,用于处理鼠标和键盘事件。在这些函数内部,您可以根据event提供的信息来执行特定的操作,例如响应用户的点击或按键。
    void mouseEventOccurred(const pcl::visualization::MouseEvent &event, void* viewer_void);
    void keyboardEventOccurred(const pcl::visualization::KeyboardEvent &event, void* viewer_void);
    
    //下面举一个模板
    viewer->registerKeyboardCallback(keyboardEventOccurred, (void*)&viewer);
    viewer->registerMouseCallback(mouseEventOccurred, (void*)&viewer);
    // mouseEventOccurred
    void mouseEventOccurred(const pcl::visualization::MouseEvent &event, void* viewer_void) {
        boost::shared_ptr<pcl::visualization::PCLVisualizer> viewer = *static_cast<boost::shared_ptr<pcl::visualization::PCLVisualizer> *>(viewer_void);
        if (event.getButton() == pcl::visualization::MouseEvent::LeftButton &&
            event.getType() == pcl::visualization::MouseEvent::MouseButtonRelease) {
            // 你的鼠标处理逻辑
        }
    }
    
    // keyboardEventOccurred
    void keyboardEventOccurred(const pcl::visualization::KeyboardEvent &event, void* viewer_void) {
        boost::shared_ptr<pcl::visualization::PCLVisualizer> viewer = *static_cast<boost::shared_ptr<pcl::visualization::PCLVisualizer> *>(viewer_void);
        if (event.getKeySym() == "r" && event.keyDown()) {
            // 你的键盘处理逻辑
        }
    }


沪ICP备19023445号-2号
友情链接