参考:
https://mp.weixin.qq.com/s/GFDWOudJ08In6jFyrZ7hhghttps://mp.weixin.qq.com/s/FfHkVY-lmlOSf4jKoZqjEA
通过之前的两篇文章,从得到点云,到对点云的下采样,去离群点。接着就是对点云的平滑,计算法线,最后生成Mesh。
点云平滑
平滑也是滤波的一种,让点云看起来稍微光滑一些。存在一些不规则数据,很难用前面提到过的统计分析等滤波方法消除,就需要平滑来处理和修复漏洞。
平滑操作在配准之后。
我使用的平滑的方法是重采样,实质上是移动最小二乘(MLS)。
主要的类是 pcl::MovingLeastSquares ,用它进行重采样操作。
参数啥的我其实还是不会调,还是用的别人的东西。
//重采样平滑点云
void SmoothPointcloud(pcl::PointCloud<pcl::PointXYZRGB>::Ptr & cloud_in, pcl::PointCloud<pcl::PointXYZRGB>::Ptr & cloud_out)
{
// 对点云重采样
std::cout<<"begin smooth: size " << cloud_in->size() << std::endl;
pcl::search::KdTree<pcl::PointXYZRGB>::Ptr treeSampling(new pcl::search::KdTree<pcl::PointXYZRGB>); // 创建用于最近邻搜索的KD-Tree
pcl::MovingLeastSquares<pcl::PointXYZRGB, pcl::PointXYZRGB> mls; // 定义最小二乘实现的对象mls
mls.setSearchMethod(treeSampling); // 设置KD-Tree作为搜索方法
mls.setComputeNormals(false); //设置在最小二乘计算中是否需要存储计算的法线
mls.setInputCloud(cloud_in); //设置待处理点云
mls.setPolynomialOrder(2); // 拟合2阶多项式拟合
mls.setPolynomialFit(false); // 设置为false可以 加速 smooth
mls.setSearchRadius(0.05); // 单位m.设置用于拟合的K近邻半径
mls.process(*cloud_out); //输出
std::cout << "success smooth, size: " << cloud_out->size() << std::endl;
}
KdTree我理解为方便管理点云中的点的一个结构,通过这个二叉树可以方便的找到我们所要的某个点。
KDTree参考:
setSearchRadius 搜索某个点周围5cm处的所有点,用二阶多项式拟合这些点。
平滑点云之后,开始下一步操作:
点云的法向量计算
由于计算点云的法向量而不是Mesh的法向量,因此比较麻烦。目前是两种重建算法,使用曲面重建的方法 和 使用近似值直接从点云中推断法线。
由于刚刚接触,参考资料中使用的是后一种方法,从该点最近邻计算的协方差矩阵的特征向量和特征值的分析,直接调用PCL封装好的函数即可。
pcl::NormalEstimation 估计法线的类
//计算法线
pcl::PointCloud<pcl::Normal>::Ptr CalculateNormal(pcl::PointCloud<pcl::PointXYZRGB>::Ptr &cloud_in)
{
// 法线估计
pcl::StopWatch time;
std::cout << "begin compute normal.... " << std::endl;
pcl::NormalEstimation<pcl::PointXYZRGB, pcl::Normal> normalEstimation; //创建法线估计的对象
normalEstimation.setInputCloud(cloud_in); //输入点云
pcl::search::KdTree<pcl::PointXYZRGB>::Ptr tree(new pcl::search::KdTree<pcl::PointXYZRGB>); // 创建用于最近邻搜索的KD-Tree
normalEstimation.setSearchMethod(tree);
pcl::PointCloud<pcl::Normal>::Ptr normals(new pcl::PointCloud<pcl::Normal>); // 定义输出的点云法线
// K近邻确定方法,使用k个最近点,或者确定一个以r为半径的圆内的点集来确定都可以,两者选1即可
normalEstimation.setKSearch(10); // 使用当前点周围最近的10个点
//normalEstimation.setRadiusSearch(0.03); //对于每一个点都用半径为3cm的近邻搜索方式
normalEstimation.compute(*normals); //计算法线
std::cout << "success compute normal,time(s): "<< time.getTimeSeconds() << std::endl;
return normals;
}
这个函数是我写的函数,接受输入点云Ptr,返回法向量Ptr。normalEstimation.setKSearch(10) 这个参数对于法向量的估计影响还是比较大的,不能太大,也不能太小。
之后在计算好了法向量,就可以得到Mesh了。
贪心三角化算法得到Mesh
点云构成三角网格的流程。Mesh存储的信息为:顶点索引,边索引,面片索引。
目前网格生成分为两大类算法,插值法(通过原始数据点得到)和逼近法(用分片线性曲面逼近原始点集)。
pcl::GreedyProjectionTriangulation 贪心三角化算法计算Mesh,使用贪心投影三角化对有向点云进行三角化,它更适用于采样点云来自表面连续光滑的曲面,并且点云的密度变化比较均匀的情况。
流程(参考资料上很详细,PCL都封装好了):
- 先将点云根据法向量投影到某个二维平面上(为某一个样本三角面片);
- 对该平面内的投影后的点云做三角化(基于Delaunay三角剖分 )得到投影后的点的拓扑连接关系
- 得到拓扑连接之后各三维点的拓扑链接就出来了,得到了Mesh。
在平滑之后要处理掉 InvalidPoints 否则无法进行Mesh。
#include <pcl/kdtree/kdtree_flann.h>
#include <pcl/features/normal_3d_omp.h>
#include <pcl/features/normal_3d.h>
#include <pcl/surface/mls.h> //最小二乘 重采样平滑
#include <pcl/surface/poisson.h> //泊松重建
#include <pcl/geometry/polygon_mesh.h> //MESH
#include <pcl/surface/gp3.h> //贪心三角形
//贪心三角化算法得到Mesh
pcl::PolygonMesh greedy_traingle_GenerateMesh(pcl::PointCloud<pcl::PointXYZRGB>::Ptr cloud_in, pcl::PointCloud<pcl::Normal>::Ptr normals)
{
pcl::StopWatch time;
std::cout << "begin mesh..." << std::endl;
pcl::PointCloud<pcl::PointXYZRGB>::Ptr cloud_out(new pcl::PointCloud<pcl::PointXYZRGB>());
SmoothPointcloud(cloud_in, cloud_out);
EraseInvalidPoints(cloud_out);
// 将点云位姿、颜色、法线信息连接到一起
pcl::PointCloud<pcl::PointXYZRGBNormal>::Ptr cloud_with_normals(new pcl::PointCloud<pcl::PointXYZRGBNormal>);
pcl::concatenateFields(*cloud_in, *normals, *cloud_with_normals);
//定义搜索树对象
pcl::search::KdTree<pcl::PointXYZRGBNormal>::Ptr tree2(new pcl::search::KdTree<pcl::PointXYZRGBNormal>);
tree2->setInputCloud(cloud_with_normals);
// 三角化
pcl::GreedyProjectionTriangulation<pcl::PointXYZRGBNormal> gp3; // 定义三角化对象
pcl::PolygonMesh triangles; //存储最终三角化的网络模型
// 设置三角化参数
gp3.setSearchRadius(0.1); //设置搜索时的半径,也就是KNN的球半径
gp3.setMu(2.5); //设置样本点搜索其近邻点的最远距离为2.5倍(典型值2.5-3),这样使得算法自适应点云密度的变化
gp3.setMaximumNearestNeighbors(100); //设置样本点最多可搜索的邻域个数,典型值是50-100
gp3.setMinimumAngle(M_PI / 18); // 设置三角化后得到的三角形内角的最小的角度为10°
gp3.setMaximumAngle(2 * M_PI / 3); // 设置三角化后得到的三角形内角的最大角度为120°
gp3.setMaximumSurfaceAngle(M_PI / 4); // 设置某点法线方向偏离样本点法线的最大角度45°,如果超过,连接时不考虑该点
gp3.setNormalConsistency(false); //设置该参数为true保证法线朝向一致,设置为false的话不会进行法线一致性检查
gp3.setInputCloud(cloud_with_normals); //设置输入点云为有向点云
gp3.setSearchMethod(tree2); //设置搜索方式
gp3.reconstruct(triangles); //重建提取三角化
cloud_with_normals->width = cloud_with_normals->height = 0;
std::cout << "success traingles, time(s) "<< time.getTimeSeconds() << std::endl;
return triangles;
}
pcl::concatenateFields 的两个输入点云的大小一定要是一样的(点云个数),否则在处理的时候会报错。
点云到Mesh的流程大概就是:
下采样,滤波,平滑,法向量,Mesh
Mesh的效果和点云质量相关。我这里用的是RGB点云,Mesh里面就自带颜色的,因此没有Texture也可以看到颜色(否则还得自己做Texture,UV啥的都不会),如果要导入Unity,就得改一下输出格式和Shader。
这些流程不是最好的,可以做优化,等我学的多一些了。。。应该会做一下吧