PCL 和 solid state Lidar SLAM
在这里主要的目的是想要使用固态激光器solid state lidar设备来实现SLAM算法。
- PCL库浏览,并且分析可能会固态激光SLAM有帮助的部分
- 固态激光器介绍
- 激光SLAM实现 Video Lidar SLAM
1. PCL学习关注点
1.1 数据结构-KDtree和八叉树
k-d tree decomposition for the point set : (2,3), (5,4), (9,6), (4,7), (8,1), (7,2)。
- 可以用来实现邻域的快速搜索。比如在ICP中,搜索最近邻点,可以设定ICP方法使用KD-Tree加速搜索。
- 底层的八叉树数据格式可以支持点云的快速压缩和合并。
- 可以使用八叉树结构分析点云数据之间的空间变化 code,通过检测体素的变化实现,可以用在motion detection上。
1.1.1 检测体素变化
下面就是使用这种方法,在ICP匹配之后寻找变化的点(如果ICP能够提供outlier就好了。。就不用这么麻烦的搞了,但是我死活没有找到接口,哪位大神知道的话指点一下)。
int GLidarSLAM::CompareAndFindOutlier(pcl::PointCloud<pcl::PointXYZRGBA>::Ptr cloud_old,
pcl::PointCloud<pcl::PointXYZRGBA>::Ptr cloud_new,
Eigen::Matrix4d transformation_matrix,
Eigen::Matrix4d transformation_matrix_to_world)
{
float octreeResolution = 1.0f;
pcl::octree::OctreePointCloudChangeDetector<pcl::PointXYZRGBA> octree(octreeResolution);
// 加入旧的点云数据
octree.setInputCloud(cloud_old);
octree.addPointsFromInputCloud();
// switch buffer
octree.switchBuffers();
// 把新的点云数据通过ICP求得的变换矩阵转移到旧点的坐标系下
pcl::PointCloud<pcl::PointXYZRGBA>::Ptr transformed_cloud(new pcl::PointCloud<pcl::PointXYZRGBA>);
pcl::transformPointCloud(*cloud_new, *transformed_cloud, transformation_matrix);
// 加入新的转换后的点云
octree.setInputCloud(transformed_cloud);
octree.addPointsFromInputCloud();
// 然后获取变化的点云数据
// retrieve the new points -> indices are index of the new point cloud
std::vector<int> newPointIdxVector;
octree.getPointIndicesFromNewVoxels(newPointIdxVector);
int nFound = newPointIdxVector.size();
}
下面是显示的结果。可以发现,虽然确实生效了,并且有一定的筛选作用,但是明显误差还是太大了。所以下面需要进一步的滤波操作才可以。
Video show
1.1.1 对结果滤波
首先使用简单的范围计数滤波
pcl::RadiusOutlierRemoval<pcl::PointXYZRGBA> outrem;
outrem.setInputCloud(cloud_tmp);
outrem.setRadiusSearch(1.0);
outrem.setMinNeighborsInRadius(30);
outrem.filter(*cloud_changed_ptr);
1.1.2 滤波之后按照欧式距离生长
在滤波之后,点云只是变化的一部分,如果要正确的获取运动物体的整体信息,还需要对过滤之后的点云进行生长。
- 可以使用PCL提供的根据Normal生长的方法,但是其实物体的normal分别不一定是均匀的,所以我觉得这种方法并不好。
- PCL也提供了根据颜色的生长方法,和上面的理由一样,不太适合运动物体的生长。
- 我觉得最适合的应该是在去除地面之后的使用欧式距离的生长。这就要求我们自己书写生长的函数。
我就简单的实现了一个按照欧式距离的生长函数。其中很多参考了PCL点云生长的源代码,最终也得到了不错的结果。
void GLidarSLAM::ConstumeRegionGrowing(pcl::PointCloud<pcl::PointXYZRGBA>::Ptr cloud_new,
pcl::PointCloud<pcl::PointXYZRGBA>::Ptr cloud_seed,
pcl::PointCloud<pcl::PointXYZRGBA>::Ptr cloud_grown)
{
float distanceThreshold = 1.0f;
// expand the changed points using KD Tree search
// creates kdtree object
// 使用KD Tree生长,这里先新建树,并且设定点云和参数
pcl::KdTreeFLANN<pcl::PointXYZRGBA> kdtree;
// sets our randomly created cloud as the input
kdtree.setInputCloud(cloud_new);
// K nearest neighbor search
int neighbour_number = 20;
int point_number = static_cast<int> (cloud_seed->points.size());
std::vector<int> neighbours;
std::vector<float> distances;
std::vector<std::vector<int>> point_neighbours;
std::vector<std::vector<float>> point_distances;
point_neighbours.resize(point_number, neighbours);
point_distances.resize(point_number, distances);
// for each points search its kdtree neighbours
// 对每一个seed点云中的点云进行生长,然后将结果全部保存下来。
for (int i_point = 0; i_point < point_number; i_point++)
{
neighbours.clear();
distances.clear();
if (!pcl::isFinite (cloud_seed->points[i_point]))
continue;
kdtree.nearestKSearch(cloud_seed->points[i_point], neighbour_number, neighbours, distances);
// 使用swap操作,可以大大节省对内存的操作
point_neighbours[i_point].swap(neighbours);
point_distances[i_point].swap(distances);
}
std::cout << " Find " << neighbour_number << " Kd tree neighbours. \n";
// 之后对邻域点分别处理,如果满足条件就把它加入输出的结果。
// 这是只是简单的使用欧式距离过滤,其实可以使用其他很多的方式筛选,这就不列举了
std::vector<int> indicesAdded;
cloud_grown->clear();
// check and add the points
for (int i = 0; i < point_number; i++){
for(int k = 0 ; k < neighbour_number; k++){
int index = point_neighbours[i][k];
if(point_distances[i][k] > distanceThreshold)
continue;
if(CheckExistance(indicesAdded, index))
continue;
indicesAdded.push_back(index);
pcl::PointXYZRGBA pt = cloud_new->points[index];
cloud_grown->points.push_back(pt);
}
}
cloud_grown->width = cloud_grown->points.size();
cloud_grown->height = 1;
cloud_grown->is_dense = false;
std::cout << " Have grown " << indicesAdded.size() << " points. \n";
}
最终的结果可以见下图和视频。算法很不错的得到了迎面行驶而来的车辆,但是明显的问题也存在:生长的结果明显还不够完善,可以考虑进行多轮生长。
Video Show
1.2 可视化
暂时不关注太多的内容,因为更习惯用pangolin。
1.3 点云滤波
-
直通滤波PassTrough,简单去除指定区域范围的点。
-
VoxelGrid体素滤波。每个体素保留一个数据(可以选择存储的是体素三维中心或者体素所有点云的重心)
-
StatisticalOutlierRemoval按统计规律移除outlier。比如使用KNN过滤(之前使用这种方法过滤SFM稠密重建点云上,得到了比较理想的结果)。
-
使用参数模型投影点云。(比如将点云投影到相机平面,用来和视觉SLAM结合)
-
ExtractIndices滤波分割点云。之前尝试使用SACSegmentation分割提取平面,效果很好,但是不够稳定,需要再研究一下。
-
ConditionalRemoval(可以自定义滤波条件)和RadiusOutlierRemoval(删除孤立的点)。
-
CropHull使用二维结构分割点云。
-
RandomFilter:
void RandomDownSample(pcl::PointCloud<pcl::PointXYZ>::Ptr& cloud, float rate)
{
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_temp(new pcl::PointCloud<pcl::PointXYZ>);
(*cloud_temp) += (*cloud);
cloud->clear();
for(size_t i = 0; i < cloud_temp->points.size();i++)
{
float r = float(rand_between(0, 100)) / 100.0;
if(r < rate)
{
pcl::PointXYZ pt = cloud_temp->points[i];
cloud->points.push_back(pt);
}
}
cloud->width = cloud->points.size();
cloud->height = 1;
cloud->is_dense = false;
}
在这里可能会用的到的有:ExtractIndices的分割点云,ConditionalRemoval自定义条件的滤波。
1.4 深度图像
- 可以用来从点云生成深度图像。这里我自己设定了相机参数,并且将点云投影。
UVandZ VirtualCamera::ProjectPointToImage(float x, float y, float z)
{
UVandZ result;
float inv_z = 1/(z);
result.u = (fx * x) * inv_z + cx;
result.v = (fy * y) * inv_z + cy;
result.z = z;
return result;
}
image = cv::Mat(rows, cols, CV_8UC3, cv::Scalar(0,0,0));
depth = cv::Mat(rows, cols, CV_32FC1, cv::Scalar(-1));
int SpeadPixel = 3;
int pointsize = cloud->points.size();
std::cout << " points count : " << pointsize << std::endl;
for(int i = 0; i < pointsize; ++i)
{
int r = cloud->points[i].r;
int g = cloud->points[i].g;
int b = cloud->points[i].b;
float x = cloud->points[i].x;
float y = cloud->points[i].y;
float z = cloud->points[i].z;
//printf("x: %f, y : %f, z : %f. \n",x, y, z);
UVandZ projected = ProjectPointToImage(x, y, z);
//printf("u: %d, v : %d, z : %f. \n",projected.u, projected.v, projected.z);
if(!PointInRange(projected)){
continue;
}
cv::circle(image, cv::Point2f(projected.v, projected.u), SpeadPixel, cv::Scalar(b,g,r), -1);
cv::circle(depth, cv::Point2f(projected.v, projected.u), SpeadPixel, cv::Scalar(z/100), -1);
/*
depth.at<float>(projected.u, projected.v) = z;
image.at<cv::Vec3b>(projected.u, projected.v)[0] = b;
image.at<cv::Vec3b>(projected.u, projected.v)[1] = g;
image.at<cv::Vec3b>(projected.u, projected.v)[2] = r;
*/
}
- use Range Image. good reference csdn
- 可以从深度图像中提取边界(岭纬的数据就是深度图像,可以提取边界,不知道能不能有帮助)
- 深度图可以之间连接三角形建模(估计对我没什么用)
1.5 关键点-深度图像
之前就考虑想用,非常适合岭纬的传感器,不知道用来SLAM追踪效果怎么样(TODO)
- NARF(normal aligned radial feature)从深度图像,提取的是边缘和表面。
- Harris 通过harris矩阵的对应的特征值判断,获得角点。点云中使用的法向量的信息。
- SIFT(暂时还不知道于图像SIFT的区别 TODO)
1.6 采样一致性RANSAC
- RandomSampleConsensus没什么好讲的。
1.7 3D点云特征于描述子
histograms
- 法向估计(不知道为什么放在这里,这个很重要)。有法向之后就可以做很多事情了。
- PFH(point feature histograms)。使用多维直方图对点的领域点云描述。Fast PFH,快了,肯定是有损失的。
- VFH(viewpoint feature histogram),将视点方向融入FPFH的相对法线角计算中。
others
- RoPS(Rotational Projection Statistics),旋转投影统计。
- 惯性矩和偏心率(moment of inertia, eccentricity)
总的来说,感觉还是深度图像的分割更适合我们的情境
1.7.1 Normal Estimation
总的来说就是统计当前点周围的点云。分析它的Covariance matrix,观察它的特征向量(有点类似主成分分析PCA)。主要特征值对应的特征向量就是法向的方向。
代码实现的部分代码:
// 1. calculate the centorid
Eigen::Vector3d centroid(0,0,0);
for(auto point : nearPoints){
centroid = centroid + point;
}
centroid = centroid/nearPoints.size();
Eigen::Matrix3d sigma = Eigen::Matrix3d::Zero();
for(auto point : nearPoints){
Eigen::Vector3d tmp = point - centroid;
sigma = sigma + tmp * tmp.transpose();
}
sigma = sigma/nearPoints.size();
Eigen::JacobiSVD<Eigen::Matrix3d> svd(sigma, Eigen::ComputeFullU | Eigen::ComputeFullV);
Eigen::Matrix3d U = svd.matrixU();
normal(0) = U(0,1);
normal(1) = U(1,1);
normal(2) = U(2,1);
PCL中的实现
pcl::NormalEstimation<pcl::PointXYZ,pcl::Normal> normalEstimation;
normalEstimation.setInputCloud(cloud);
pcl::search::KdTree<pcl::PointXYZ>::Ptr kdtree(new pcl::search::KdTree<pcl::PointXYZ>);
normalEstimation.setSearchMethod(kdtree);
// choose one of the following two methods
normalEstimation.setKSearch(20);
//normalEstimation.setRadiusSearch(0.3);
normalEstimation.compute(*normals);
1.8 点云标配
- ICP,细节不说了。我觉得我可以考虑先计算法相之后用点面ICP计算比较一下结果(TODO)。
- NDT
- 另外,可以使用openmp版本的GICP和NDT。
- 另外在这一章中,有各种描述子的匹配例子,值得学习一下,理解不同描述子的优劣,以备之后运用。
1.8.1 ICP
1.9 点云分割
- 平面和各种几何的分割,大多使用SACSegmentation类来实现。
- 欧式距离聚类。
- 区域生长region growing(可以基于颜色,距离,法向等)
- 另外graph cut也可以使用在这里。但是这个大多只用在背景于前景的分割上,要看清使用的情境。
- 法向分割
个人认为提取平面之后的据类会很有效,但是如果有其他办法提取运动物体,那就不需要分割了
1.10 点云曲面重建
- Convex Hull : Ear Clipping, Delaunay, Marching cube, Poission
也就提供了这些最经典的算法了,需要跳出这个圈子,寻找更加适合我们场景的算法。
2. 激光设备
2.1 Traditional Lidar
2.2 Solid state Lidar
reference page 1
reference page 2
2.3 固态激光供应商
我只用过岭纬的设备,总体感觉非常理想,数据精度很高,而且视觉于激光的标配很让人满意。是理想的视觉激光融合SLAM开发的设备。
- 激光数据准确,就可以粗暴的使用RANSAC ICP算法。
- 激光点稠密,计算的normal也更加准确,也很有利于点云分割。
- 有RGB图像数据提供,可以完美的使用视觉激光融合(像升级版的RGBD相机,但是由于激光的准确性,后端优化的任务大幅度降低!)。
2.3.1 Neuvition岭纬
- 480线, 水平分辨率可以达到1750
- 分辨率 0.04° * 0.05°
- MEMS技术
2.3.2 RoboSense速腾
- 分辨率0.1° * 0.25°
- MEMS技术
2.3.3 Velodyne
- 将在2020年投产
- 具体的参数也不清楚