A_loam的laserMapping.cpp约束构建理解

一、A_loam的主要过程

​​​​​​​        采用提取点云的面特征(曲率小)、线特征(曲率大),构建点-线约束、点-面约束,采用ceres非线性优化进行匹配. 粗匹配(快,Scan2Scan)+精匹配(慢,Scan2Map)模式。

1)scanRegistration.cpp

        完成两件事:点云重组(计算ring以及分类每条线束的点、每个点扫描时间)和特征寻找(每一帧点云寻找16x6x(4+2)个特征,16线束、6个方向、每个方向4个极大面特征和2个极大线特征);

2)laserOdometry.cpp

        scan2scan的粗匹配进行帧间递推,完成里程计的位姿计算(当前帧的极大特征与上一帧的次极大特征匹配,次极大特征的集合包含了极大特征集合);

3)laserMapping.cpp

        scan2map的精匹配,当前帧特征在submap特征中最近邻寻找,找到后,采用类似PCA(求点云的协方差矩阵的特征值和特征向量)or 构建超定方程(QR分解)去检验submap里面的特征,是否满足的线方程 or 平面方程,满足后,用当前帧的特征点和求解的方程去构建优化问题(laserOdom的估计值会在这里提供初值)。

二、代码理解

        A_loam的laserOdom估计出来的位姿结果是比较一般的,所以后面有作者发布了F_LOAM, 直接去除了该节点。下面的mapping部分最关键的部分,加上自己的理解的注释,直接看建图线程:std::thread mapping_process{process}:

if (laserCloudCornerFromMapNum > 10 && laserCloudSurfFromMapNum > 50)		// submap的特征点足够,则进行优化问题的构建
{
	TicToc t_opt;
	TicToc t_tree;
	kdtreeCornerFromMap->setInputCloud(laserCloudCornerFromMap);		// 输入submap特征
	kdtreeSurfFromMap->setInputCloud(laserCloudSurfFromMap);
	printf("build tree time %f ms \n", t_tree.toc());

	for (int iterCount = 0; iterCount < 2; iterCount++)		// 构建最小二乘,类似laserodom, 迭代次数2
	{
		//ceres::LossFunction *loss_function = NULL;
		ceres::LossFunction *loss_function = new ceres::HuberLoss(0.1);
		ceres::LocalParameterization *q_parameterization = new ceres::EigenQuaternionParameterization();	// ! 四元素作为位姿优化

		ceres::Problem::Options problem_options;
		ceres::Problem problem(problem_options);		// 四元素
		problem.AddParameterBlock(parameters, 4, q_parameterization);		// parameters为7个元素的数组,前4个姿态,后3个位移
		problem.AddParameterBlock(parameters+4, 3);	

		TicToc t_data;
		int corner_num = 0;
		/**
		 * 如果是边线点,则以这五个点的均值点为中心,以5个点的主方向向量(类似于PCA方法)为方向,作一条直线,令该边线点与直线距离最短,构建非线性优化问题;
		 * 如果是平面点,则寻找五个点的法方向(反向的PCA方法),令这个平面点在法方向上与五个近邻点的距离最小,构建非线性优化问题。
		*/
		for (int i = 0; i < laserCloudCornerStackNum; i++) 		// 当前帧 特征点云大小  // ! 线特征约束构建
		{	
			pointOri = laserCloudCornerStack->points[i];		// 当前帧起始点
			//double sqrtDis = pointOri.x * pointOri.x + pointOri.y * pointOri.y + pointOri.z * pointOri.z;
			pointAssociateToMap(&pointOri, &pointSel);	// 该点坐标转到map下,开始寻找关联
			kdtreeCornerFromMap->nearestKSearch(pointSel, 5, pointSearchInd, pointSearchSqDis); 	// 最近邻5个点

			if (pointSearchSqDis[4] < 1.0)
			{ 
				std::vector<Eigen::Vector3d> nearCorners;		// 存储搜索出的5个特征点
				Eigen::Vector3d center(0, 0, 0);
				for (int j = 0; j < 5; j++)
				{
					Eigen::Vector3d tmp(laserCloudCornerFromMap->points[pointSearchInd[j]].x,
										laserCloudCornerFromMap->points[pointSearchInd[j]].y,
										laserCloudCornerFromMap->points[pointSearchInd[j]].z);
					center = center + tmp;
					nearCorners.push_back(tmp);
				}
				center = center / 5.0;		// 五个点的均值

				Eigen::Matrix3d covMat = Eigen::Matrix3d::Zero();	// ! 构建5个最近邻点的协方差矩阵
				for (int j = 0; j < 5; j++)
				{
					Eigen::Matrix<double, 3, 1> tmpZeroMean = nearCorners[j] - center;
					covMat = covMat + tmpZeroMean * tmpZeroMean.transpose();	
				}
				// 对协方差矩阵进行特征值分解
				Eigen::SelfAdjointEigenSolver<Eigen::Matrix3d> saes(covMat);

				// if is indeed line feature
				// note Eigen library sort eigenvalues in increasing order
				// ! PCA的原理:计算协方差矩阵的特征值和特征向量,用于判断这5个点是不是呈线状分布
				// ! 如果5个点呈线状分布,最大的特征值对应的特征向量就是该线的方向向量
				// eigenvectors分解的特征向量组成的矩阵, eigenvalues特征值(内部应该是按照升序排列的)
				Eigen::Vector3d unit_direction = saes.eigenvectors().col(2);	// 如果5个点呈线状分布,最大的特征值对应的特征向量就是该线的方向向量
				Eigen::Vector3d curr_point(pointOri.x, pointOri.y, pointOri.z);
				if (saes.eigenvalues()[2] > 3 * saes.eigenvalues()[1])		// 如果最大的特征值 远大于>> 其他特征值,则5个点确实呈线状分布,否则认为直线“不够直”
				{ 
					Eigen::Vector3d point_on_line = center;		// 中心点
					Eigen::Vector3d point_a, point_b;
					// 根据拟合出来的线特征方向,以平均点为中心构建两个虚拟点,代替一条直线
					point_a = 0.1 * unit_direction + point_on_line;
					point_b = -0.1 * unit_direction + point_on_line;
					// 构建约束
					ceres::CostFunction *cost_function = LidarEdgeFactor::Create(curr_point, point_a, point_b, 1.0);
					problem.AddResidualBlock(cost_function, loss_function, parameters, parameters + 4);
					corner_num++;	
				}							
			}
			/*
			else if(pointSearchSqDis[4] < 0.01 * sqrtDis)
			{
				Eigen::Vector3d center(0, 0, 0);
				for (int j = 0; j < 5; j++)
				{
					Eigen::Vector3d tmp(laserCloudCornerFromMap->points[pointSearchInd[j]].x,
										laserCloudCornerFromMap->points[pointSearchInd[j]].y,
										laserCloudCornerFromMap->points[pointSearchInd[j]].z);
					center = center + tmp;
				}
				center = center / 5.0;	
				Eigen::Vector3d curr_point(pointOri.x, pointOri.y, pointOri.z);
				ceres::CostFunction *cost_function = LidarDistanceFactor::Create(curr_point, center);
				problem.AddResidualBlock(cost_function, loss_function, parameters, parameters + 4);
			}
			*/
		}

		int surf_num = 0;
		for (int i = 0; i < laserCloudSurfStackNum; i++)		// ! 【2】面特征约束构建
		{
			pointOri = laserCloudSurfStack->points[i];
			//double sqrtDis = pointOri.x * pointOri.x + pointOri.y * pointOri.y + pointOri.z * pointOri.z;
			pointAssociateToMap(&pointOri, &pointSel);
			kdtreeSurfFromMap->nearestKSearch(pointSel, 5, pointSearchInd, pointSearchSqDis);

			Eigen::Matrix<double, 5, 3> matA0;
			Eigen::Matrix<double, 5, 1> matB0 = -1 * Eigen::Matrix<double, 5, 1>::Ones();
			// 构建平面方程Ax + By +Cz + 1 = 0
			// 通过构建一个超定方程来求解这个平面方程
			if (pointSearchSqDis[4] < 1.0)
			{
				
				for (int j = 0; j < 5; j++)
				{
					matA0(j, 0) = laserCloudSurfFromMap->points[pointSearchInd[j]].x;
					matA0(j, 1) = laserCloudSurfFromMap->points[pointSearchInd[j]].y;
					matA0(j, 2) = laserCloudSurfFromMap->points[pointSearchInd[j]].z;
					//printf(" pts %f %f %f ", matA0(j, 0), matA0(j, 1), matA0(j, 2));
				}
				// find the norm of plane
				// 调用eigen接口求解该方程,解就是这个平面的法向量
				// ! matA0 * norm = matB0, matB0是0矩阵, 那么求得的norm即为法向量 
				Eigen::Vector3d norm = matA0.colPivHouseholderQr().solve(matB0);
				double negative_OA_dot_norm = 1 / norm.norm();			// 平方开方,2范数
				norm.normalize(); 		// 后面校验的时候norm用的归一化去做的,所以平面方程的常数项1也要归一化

				// Here n(pa, pb, pc) is unit norm of plane
				// 根据求出来的平面方程进行校验,看看是不是符合平面约束
				bool planeValid = true;		// !校验该平面是否成立
				for (int j = 0; j < 5; j++)
				{
					// if OX * n > 0.2, then plane is not fit well
					//! 点(x0, y0, z0)到平面Ax + By + Cz + D = 0 的距离
					if (fabs(norm(0) * laserCloudSurfFromMap->points[pointSearchInd[j]].x +
								norm(1) * laserCloudSurfFromMap->points[pointSearchInd[j]].y +
								norm(2) * laserCloudSurfFromMap->points[pointSearchInd[j]].z + negative_OA_dot_norm) > 0.2) {
						planeValid = false;
						break;
					}
				}
				Eigen::Vector3d curr_point(pointOri.x, pointOri.y, pointOri.z);
				if (planeValid)
				{
					// 构造点到面的距离残差项
					ceres::CostFunction *cost_function = LidarPlaneNormFactor::Create(curr_point, norm, negative_OA_dot_norm);
					problem.AddResidualBlock(cost_function, loss_function, parameters, parameters + 4);
					surf_num++;
				}
			}
			/*
			else if(pointSearchSqDis[4] < 0.01 * sqrtDis)
			{
				Eigen::Vector3d center(0, 0, 0);
				for (int j = 0; j < 5; j++)
				{
					Eigen::Vector3d tmp(laserCloudSurfFromMap->points[pointSearchInd[j]].x,
										laserCloudSurfFromMap->points[pointSearchInd[j]].y,
										laserCloudSurfFromMap->points[pointSearchInd[j]].z);
					center = center + tmp;
				}
				center = center / 5.0;	
				Eigen::Vector3d curr_point(pointOri.x, pointOri.y, pointOri.z);
				ceres::CostFunction *cost_function = LidarDistanceFactor::Create(curr_point, center);
				problem.AddResidualBlock(cost_function, loss_function, parameters, parameters + 4);    // 最终优化输出 parameters[7]
			}
			*/
		}

		//printf("corner num %d used corner num %d \n", laserCloudCornerStackNum, corner_num);
		//printf("surf num %d used surf num %d \n", laserCloudSurfStackNum, surf_num);

		printf("mapping data assosiation time %f ms \n", t_data.toc());
		// ! ceres求解
		TicToc t_solver;
		ceres::Solver::Options options;
		options.linear_solver_type = ceres::DENSE_QR;
		options.max_num_iterations = 4;
		options.minimizer_progress_to_stdout = false;
		options.check_gradients = false;
		options.gradient_check_relative_precision = 1e-4;
		ceres::Solver::Summary summary;
		ceres::Solve(options, &problem, &summary);
		printf("mapping solver time %f ms \n", t_solver.toc());

		//printf("time %f \n", timeLaserOdometry);
		//printf("corner factor num %d surf factor num %d\n", corner_num, surf_num);
		//printf("result q %f %f %f %f result t %f %f %f\n", parameters[3], parameters[0], parameters[1], parameters[2],
		//	   parameters[4], parameters[5], parameters[6]);
	}
	printf("mapping optimization time %f \n", t_opt.toc());
}

        上面的点到线的ICP过程就比Odometry中的要鲁棒和准确一些了(当然也就更耗时一些),因为不是简单粗暴地搜上一帧的2个corner点作为target的线,而是PCA计算出最近邻的5个点的主方向作为线的方向。

        上面的点到面的ICP过程同样比Odometry中的要鲁棒和准确一些了(当然也就更耗时一些),因为不是简单粗暴地搜最近的3个surf点作为target的平面点,而是对最近邻的5个点进行基于最小二乘的平面拟合.(构建超定方程组QR分解)。

        另外,我觉得重要的,就是lidarFactor.hpp的残差构建,原理如下:

        点线残差(我理解了好久=_=,太蠢了):

         点-面约束残差就是点的面的距离,目标点 点积面的法向即可,那个比较好理解。

        还有一点,就是比较新颖的,是源码中对submap的管理并不是基于时序的滑动窗口方式,而是空间范围划分方式,这就是为什么最后的frame对应的submap会包含早期的frames。

Last,我看过的觉得很详细的分析:LOAM笔记及A-LOAM源码阅读 - WellP.C - 博客园

感谢!!!

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值