描述

利用PCL库既有函数,对激光雷达点云进行一系列处理。
操作数据可以利用一些经典点云文件,我的操作还有一些自采数据。

经典点云数据

斯坦福兔子

猴子

龙(数据量比前两个大了不少)

代码

1. 加载点云

pcd点云格式

#include <pcl/io/pcd_io.h>

pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>());
std::string fileName = "/home/chen/CodeBase/c++_try/bunny.pcd";
pcl::io::loadPCDFile(fileName.c_str(), *cloud);
std::cout<<"input cloud points num:"<<cloud->points.size()<<std::endl;

ply点云格式

#include <pcl/io/ply_io.h>
std::string fileName = "/home/chen/CodeBase/c++_try/bunny.ply";
pcl::io::loadPLYFile(fileName.c_str(), *cloud);
std::cout<<"input cloud points num:"<<cloud->points.size()<<std::endl;

点云初始化

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

2. 显示点云

两种显示方式

PCLVisualizer

指针

#include <pcl/visualization/cloud_viewer.h>

std::shared_ptr<pcl::visualization::PCLVisualizer> viewer(new pcl::visualization::PCLVisualizer("cloud show"));
pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ> tc_handler(cloud, 0, 255, 0);
viewer->addPointCloud(cloud, tc_handler, "origin cloud");
while (!viewer->wasStopped())
{
     viewer->spinOnce(100);
}

非指针

#include <pcl/visualization/cloud_viewer.h>

pcl::visualization::PCLVisualizer viewer("cloud show");
viewer.addPointCloud(cloud, "origin cloud");
while (!viewer.wasStopped())
{
    viewer.spinOnce(100);
}

CloudViewer

pcl::visualization::CloudViewer cloud_viewer("standford bunny");
cloud_viewer.showCloud(cloud);
while (!cloud_viewer.wasStopped())
{
}

3. 去除无效点

有的时候数据文件中,会有Nan的无效数据点,一定要在其他操作之前将这些点滤除。
我吃过很多次这个亏

#include <pcl/filters/statistical_outlier_removal.h>

pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_noNan(new pcl::PointCloud<pcl::PointXYZ>);
std::vector<int> indices;
std::cout<<"cloud:"<<cloud->points.size()<<std::endl;
pcl::removeNaNFromPointCloud(*cloud, *cloud_noNan, indices);
std::cout<<"cloud no Nan:"<<cloud_noNan->points.size()<<std::endl;

4. 直通滤波

直通滤波,就是在xyz三个方向按照值的大小进行去除点。像切西瓜一样,只不过你只能按照xyz三个既定方向来切割点云

数值大小的单位,随你的数据来定,我的数据单位是m。因此如下代码,我过滤掉的x范围,是小于0大于40m范围的点,留下的点在0-40m之间

#include <pcl/filters/passthrough.h>

pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
pcl::PassThrough<pcl::PointXYZ> pass; 
pass.setInputCloud (cloud); 
pass.setFilterFieldName ("x"); 
pass.setFilterLimits (0, 40); 
pass.filter (*cloud);

pass.setInputCloud (cloud); 
pass.setFilterFieldName ("y"); 
pass.setFilterLimits (-20, 20); 
pass.filter (*cloud);

pass.setInputCloud (cloud); 
pass.setFilterFieldName ("z"); 
pass.setFilterLimits (-5, 5); 
pass.filter (*cloud);

如果设置一个范围的点是不需要的,加上这句话。平时不加这句时,参数默认是false

pass.setNegative(true);

5. 点云下采样

利用VoxelGrid滤波器来进行点云数据下采样

#include <pcl/filters/voxel_grid.h>

//Use a voxelSampler to downsample
pcl::VoxelGrid<pcl::PointXYZ> voxelSampler;
voxelSampler.setInputCloud(cloud);
voxelSampler.setLeafSize(0.01f, 0.01f, 0.01f); // 设置滤波时创建的体素体积为1cm3 的立方体
voxelSampler.filter(*cloud);

6. 去除离群点(噪声点)

使用StatisticalOutlierRemoval滤波器剔除离群点
该方法是一种基于统计分析的技术,核心思路:对每个点的邻域进行统计分析,并修剪掉那些不符合一定标准的数据点。
StatisticalOutlierRemoval滤波器的原理:对每个点,计算它和它所有临近点的平均距离。假设得到的结果是一个高斯分布,其形状是由均值和标准差决定的,平均距离在标准范围(由全局距离平均值和方差定义)之外的点,被认为是离群点进行去除。

#include <pcl/filters/statistical_outlier_removal.h>

pcl::StatisticalOutlierRemoval<pcl::PointXYZ> statFilter;
statFilter.setInputCloud(cloud);
statFilter.setMeanK(10); // 临近点个数
statFilter.setStddevMulThresh(0.2); // 判断是否为离群点的阈值
statFilter.filter(*cloud);

7. 区域生长

在我的这篇CSDN文章中,对区域生长有单独且详细的说明,这里暂不展开
https://blog.csdn.net/weixin_42156097/article/details/111474829

8. 点云外包矩形框

OBB有向包围box

一个点云,是会被一个最小的、外包的、6面的立方体包围的。计算出这一外包立方体的8个顶点,是一个挺重要的步骤。

包成了一个API,输入参数为点云,输出为8个端点坐标

#include <pcl/common/common.h>
#include <pcl/features/moment_of_inertia_estimation.h>
#include <Eigen/Core>
#include <Eigen/Eigenvalues>

std::vector<pcl::PointXYZ> compute_corners(pcl::PointCloud<pcl::PointXYZ>::Ptr cloud) {
    std::vector<pcl::PointXYZ> result;

    pcl::PointXYZ min_pt, max_pt;
    pcl::getMinMax3D(*cloud, min_pt, max_pt);

    pcl::MomentOfInertiaEstimation <pcl::PointXYZ> feature_extractor;
    feature_extractor.setInputCloud(cloud);
    feature_extractor.compute();
    pcl::PointXYZ OBB_min_pt;
    pcl::PointXYZ OBB_max_pt;
    pcl::PointXYZ OBB_cen_pt;
    Eigen::Matrix3f OBB_matrix_r;
    feature_extractor.getOBB(OBB_min_pt, OBB_max_pt, OBB_cen_pt, OBB_matrix_r);
    Eigen::Vector3f position(OBB_cen_pt.x, OBB_cen_pt.y, OBB_cen_pt.z);
    Eigen::Quaternionf quat(OBB_matrix_r);
    Eigen::Vector3f p1(OBB_min_pt.x, OBB_min_pt.y, OBB_min_pt.z);
    Eigen::Vector3f p2(OBB_min_pt.x, OBB_min_pt.y, max_pt.z - OBB_cen_pt.z);
    Eigen::Vector3f p3(OBB_max_pt.x, OBB_min_pt.y, max_pt.z - OBB_cen_pt.z);
    Eigen::Vector3f p4(OBB_max_pt.x, OBB_min_pt.y, OBB_min_pt.z);
    Eigen::Vector3f p5(OBB_min_pt.x, OBB_max_pt.y, OBB_min_pt.z);
    Eigen::Vector3f p6(OBB_min_pt.x, OBB_max_pt.y, max_pt.z - OBB_cen_pt.z);
    Eigen::Vector3f p7(OBB_max_pt.x, OBB_max_pt.y, max_pt.z - OBB_cen_pt.z);
    Eigen::Vector3f p8(OBB_max_pt.x, OBB_max_pt.y, OBB_min_pt.z);
    p1 = OBB_matrix_r * p1 + position;
    p2 = OBB_matrix_r * p2 + position;
    p3 = OBB_matrix_r * p3 + position;
    p4 = OBB_matrix_r * p4 + position;
    p5 = OBB_matrix_r * p5 + position;
    p6 = OBB_matrix_r * p6 + position;
    p7 = OBB_matrix_r * p7 + position;
    p8 = OBB_matrix_r * p8 + position;

    pcl::PointXYZ pt1(p1(0), p1(1), p1(2));
    pcl::PointXYZ pt2(p2(0), p2(1), p2(2));
    pcl::PointXYZ pt3(p3(0), p3(1), p3(2));
    pcl::PointXYZ pt4(p4(0), p4(1), p4(2));
    pcl::PointXYZ pt5(p5(0), p5(1), p5(2));
    pcl::PointXYZ pt6(p6(0), p6(1), p6(2));
    pcl::PointXYZ pt7(p7(0), p7(1), p7(2));
    pcl::PointXYZ pt8(p8(0), p8(1), p8(2));

    result.push_back(pt1);
    result.push_back(pt2);
    result.push_back(pt3);
    result.push_back(pt4);
    result.push_back(pt5);
    result.push_back(pt6);
    result.push_back(pt7);
    result.push_back(pt8);

    return result;
}

如何使用上述API接口

std::vector<pcl::PointXYZ> corners;
corners = compute_corners(cloud_noNan);
std::cout<<"corners num:"<<corners.size()<<std::endl;
for (int i = 0; i < corners.size(); i++) {
    std::cout<<corners[i]<<std::endl;
}

多说一句,注意8个点的计算过程就可以知道,每个点在立方体的具体位置。
其中0、3、4、7,代表着立方体下表面的四个顶点

std::vector<pcl::PointXYZ> corners_down4points;
corners_down4points.push_back(corners[0]);
corners_down4points.push_back(corners[3]);
corners_down4points.push_back(corners[4]);
corners_down4points.push_back(corners[7]);

按照如下顺序依次连接4个顶点,可以画出下表面。下述代码使用了opencv来画出四个顶点按照顺序,给出的外包立方体的下表面

// rects_down是corners_down4points根据工程实际情况,转到图像坐标系下的点。
// 随个人工程而定
cv::line(m_image_origin, rects_down[i    ], rects_down[i + 1], cv::Scalar(0, 0, 255), 5, 8, 0);
cv::line(m_image_origin, rects_down[i + 1], rects_down[i + 3], cv::Scalar(0, 0, 255), 5, 8, 0);
cv::line(m_image_origin, rects_down[i + 3], rects_down[i + 2], cv::Scalar(0, 0, 255), 5, 8, 0);
cv::line(m_image_origin, rects_down[i + 2], rects_down[i    ], cv::Scalar(0, 0, 255), 5, 8, 0);

9. XYZ点云转XYZRGB彩色图

pcl::PointCloud<pcl::PointXYZRGB>::Ptr cloud_color(new pcl::PointCloud<pcl::PointXYZRGB>);
pcl::copyPointCloud(*cloud, *cloud_color);

size_t max_i = cloud_color->points.size();
for (size_t i = 0; i < max_i; i++) {
    cloud_color->points[i].r = (uint8_t)0;
    cloud_color->points[i].g = (uint8_t)255;
    cloud_color->points[i].b = (uint8_t)0;
}
cloud_viewer.showCloud(cloud_color);

总结

这篇文章列举了一些常用的PCL库操作。下一篇将介绍如何通过PCA主成分分析,像OBB一样提取点云的外边框

写的不容易,欢迎各位朋友点赞并加关注,谢谢!