对LOAM算法原理和代码的理解

article/2025/8/30 20:11:54

LOAM概况

V-LOAM和LOAM在KITTI上的排名一直是前两名。
LOAM在KITTI上的排名
原始LOAM代码(带中文注释)的地址:https://github.com/cuitaixiang/LOAM_NOTED
作为教学用的A-LOAM代码地址:https://github.com/HKUST-Aerial-Robotics/A-LOAM使用ceres库来做非线性优化。代码里默认一帧激光数据内是匀速运动的,直接使用时间进行线性插值得到去畸变的点云位姿

LOAM使用两轴的激光雷达进行SLAM建图。其具体的结构如下图所示:
在这里插入图片描述
因为一帧完整的激光数据所经历的时间比较长,在该段时间内激光器的位移已不能忽略。所以每束激光测量的距离会在激光器的不同位置获得。LOAM首先面临的问题就是点云运动畸变的去除。另外,LOAM将SLAM问题分成了两部分,使用两个算法并行运行来处理这两部分问题,以达到实时构建低漂移的地图。其中一个算法以10Hz的频率运行,得到一个 较为粗糙的里程计信息。该算法使用基于特征点的scan-to-scan match方法,可以快速计算得到里程计信息。另一个算法以1Hz的频率运行,输出一个更为精确的里程计信息。因为它以一个更低的频率运行,所以可以获取更多的特征点,同时也可以进行更多次迭代。总的来说,LOAM结合了scan-to-scan match方法和scan-to-map match方法的优势,很好地控制了运算速度和精度。

LOAM主要算法流程

在这里插入图片描述
上图为LOAM算法的主要执行流程。下面分别说明。
Point Cloud Registration
1.使用IMU数据进行点云运动畸变去除

利用IMU去除激光传感器在运动过程中非匀速(加减速)部分造成的误差(运动畸变)。为什么这样做呢?因为LOAM是基于匀速运动的假设,但实际中激光传感器的运动肯定不是匀速的,因此使用IMU来去除非匀速运动部分造成的误差,以满足匀速运动的假设。

imuHandler()函数接受IMU数据。IMU的数据可以提供给我们IMU坐标系三个轴相对于世界坐标系的欧拉角和三个轴上的加速度。但由于加速度受到重力的影响所以首先得去除重力影响。在去除重力影响后我们想要获得IMU在世界坐标系下的运动,因此在 AccumulateIMUShift()中,根据欧拉角就可以将IMU三轴上的加速度转换到世界坐标系下的加速度。 然后利用匀加速度公式 s 1 = s 2 + v t + 1 / 2 a t 2 s1 = s2+ vt + 1/2at^2 s1=s2+vt+1/2at2来计算位移。因此我们可以求出每一帧IMU数据在世界坐标系下对应的位移和速度。代码中实际缓存了200帧这样的数据,为后面插值激光点运动畸变打下了基础。

这里使用IMU数据进行插值计算点云的中点的位置,消除由于非匀速运动造成的运动畸变。

//-0.5 < relTime < 1.5(点旋转的角度与整个周期旋转角度的比率, 即点云中点的相对时间)float relTime = (ori - startOri) / (endOri - startOri);//点强度=线号+点相对时间(即一个整数+一个小数,整数部分是线号,小数部分是该点的相对时间),匀//速扫描:根据当前扫描的角度和扫描周期计算相对扫描起始位置的时间point.intensity = scanID + scanPeriod * relTime;//点时间=点云时间+周期时间if (imuPointerLast >= 0) {//如果收到IMU数据,使用IMU矫正点云畸变float pointTime = relTime * scanPeriod;//计算点的周期时间//寻找是否有点云的时间戳小于IMU的时间戳的IMU位置:imuPointerFrontwhile (imuPointerFront != imuPointerLast) {if (timeScanCur + pointTime < imuTime[imuPointerFront]) {break;}imuPointerFront = (imuPointerFront + 1) % imuQueLength;}if (timeScanCur + pointTime > imuTime[imuPointerFront]) {//没找到,此时
//imuPointerFront==imtPointerLast,只能以当前收到的最新的IMU的速度,位移,欧拉角作为当前点的
//速度,位移,欧拉角使用imuRollCur = imuRoll[imuPointerFront];imuPitchCur = imuPitch[imuPointerFront];imuYawCur = imuYaw[imuPointerFront];imuVeloXCur = imuVeloX[imuPointerFront];imuVeloYCur = imuVeloY[imuPointerFront];imuVeloZCur = imuVeloZ[imuPointerFront];imuShiftXCur = imuShiftX[imuPointerFront];imuShiftYCur = imuShiftY[imuPointerFront];imuShiftZCur = imuShiftZ[imuPointerFront];} else {//找到了点云时间戳小于IMU时间戳的IMU位置,则该点必处于imuPointerBack和
//imuPointerFront之间,据此线性插值,计算点云点的速度,位移和欧拉角int imuPointerBack = (imuPointerFront + imuQueLength - 1) % imuQueLength;//按时间距离计算权重分配比率,也即线性插值float ratioFront = (timeScanCur + pointTime - imuTime[imuPointerBack]) / (imuTime[imuPointerFront] - imuTime[imuPointerBack]);float ratioBack = (imuTime[imuPointerFront] - timeScanCur - pointTime) / (imuTime[imuPointerFront] - imuTime[imuPointerBack]);imuRollCur = imuRoll[imuPointerFront] * ratioFront + imuRoll[imuPointerBack] * ratioBack;imuPitchCur = imuPitch[imuPointerFront] * ratioFront + imuPitch[imuPointerBack] * ratioBack;if (imuYaw[imuPointerFront] - imuYaw[imuPointerBack] > M_PI) {imuYawCur = imuYaw[imuPointerFront] * ratioFront + (imuYaw[imuPointerBack] + 2 * M_PI) * ratioBack;} else if (imuYaw[imuPointerFront] - imuYaw[imuPointerBack] < -M_PI) {imuYawCur = imuYaw[imuPointerFront] * ratioFront + (imuYaw[imuPointerBack] - 2 * M_PI) * ratioBack;} else {imuYawCur = imuYaw[imuPointerFront] * ratioFront + imuYaw[imuPointerBack] * ratioBack;}//本质:imuVeloXCur = imuVeloX[imuPointerback] + (imuVelX[imuPointerFront]-imuVelX[imuPoniterBack])*ratioFrontimuVeloXCur = imuVeloX[imuPointerFront] * ratioFront + imuVeloX[imuPointerBack] * ratioBack;imuVeloYCur = imuVeloY[imuPointerFront] * ratioFront + imuVeloY[imuPointerBack] * ratioBack;imuVeloZCur = imuVeloZ[imuPointerFront] * ratioFront + imuVeloZ[imuPointerBack] * ratioBack;imuShiftXCur = imuShiftX[imuPointerFront] * ratioFront + imuShiftX[imuPointerBack] * ratioBack;imuShiftYCur = imuShiftY[imuPointerFront] * ratioFront + imuShiftY[imuPointerBack] * ratioBack;imuShiftZCur = imuShiftZ[imuPointerFront] * ratioFront + imuShiftZ[imuPointerBack] * ratioBack;}if (i == 0) {//如果是第一个点,记住点云起始位置的速度,位移,欧拉角imuRollStart = imuRollCur;imuPitchStart = imuPitchCur;imuYawStart = imuYawCur;imuVeloXStart = imuVeloXCur;imuVeloYStart = imuVeloYCur;imuVeloZStart = imuVeloZCur;imuShiftXStart = imuShiftXCur;imuShiftYStart = imuShiftYCur;imuShiftZStart = imuShiftZCur;} else {//计算之后每个点相对于第一个点的由于加减速非匀速运动产生的位移速度畸变,并对点云中
//的每个点位置信息重新补偿矫正ShiftToStartIMU(pointTime);VeloToStartIMU();TransformToStartIMU(&point);}}laserCloudScans[scanID].push_back(point);//将每个补偿矫正的点放入对应线号的容器
}

void ShiftToStartIMU(float pointTime)函数中主要计算一帧数据中某点相对于一帧的起始点由于加减速造成的运动畸变。因此我们首先要求出世界坐标系下的加减速造成的运动畸变,然后将运动畸变值经过绕y、x、z轴旋转后得到起始点坐标系下的运动畸变。这里的坐标系一定要搞清楚为什么要放的起始点的坐标系下。

//计算局部坐标系下点云中的点相对第一个开始点的由于加减速运动产生的位移畸变
void ShiftToStartIMU(float pointTime)
{//计算相对于第一个点由于加减速产生的畸变位移(全局坐标系下畸变位移量delta_Tg)//imuShiftFromStartCur = imuShiftCur - (imuShiftStart + imuVeloStart * pointTime)imuShiftFromStartXCur = imuShiftXCur - imuShiftXStart - imuVeloXStart * pointTime;imuShiftFromStartYCur = imuShiftYCur - imuShiftYStart - imuVeloYStart * pointTime;imuShiftFromStartZCur = imuShiftZCur - imuShiftZStart - imuVeloZStart * pointTime;/********************************************************************************Rz(pitch).inverse * Rx(pitch).inverse * Ry(yaw).inverse * delta_Tgtransfrom from the global frame to the local frame*********************************************************************************///绕y轴旋转(-imuYawStart),即Ry(yaw).inversefloat x1 = cos(imuYawStart) * imuShiftFromStartXCur - sin(imuYawStart) * imuShiftFromStartZCur;float y1 = imuShiftFromStartYCur;float z1 = sin(imuYawStart) * imuShiftFromStartXCur + cos(imuYawStart) * imuShiftFromStartZCur;//绕x轴旋转(-imuPitchStart),即Rx(pitch).inversefloat x2 = x1;float y2 = cos(imuPitchStart) * y1 + sin(imuPitchStart) * z1;float z2 = -sin(imuPitchStart) * y1 + cos(imuPitchStart) * z1;//绕z轴旋转(-imuRollStart),即Rz(pitch).inverseimuShiftFromStartXCur = cos(imuRollStart) * x2 + sin(imuRollStart) * y2;imuShiftFromStartYCur = -sin(imuRollStart) * x2 + cos(imuRollStart) * y2;imuShiftFromStartZCur = z2;
}

接下来就是VeloToStartIMU()函数,这个函数流程和上一个函数大致相同。它的作用就是求当前点的速度相对于点云起始点的速度畸变,先计算全局坐标系下的然后再转换到起始点的坐标系中。

//计算局部坐标系下点云中的点相对第一个开始点由于加减速产生的的速度畸变(增量)
void VeloToStartIMU()
{//计算相对于第一个点由于加减速产生的畸变速度(全局坐标系下畸变速度增量delta_Vg)imuVeloFromStartXCur = imuVeloXCur - imuVeloXStart;imuVeloFromStartYCur = imuVeloYCur - imuVeloYStart;imuVeloFromStartZCur = imuVeloZCur - imuVeloZStart;/********************************************************************************Rz(pitch).inverse * Rx(pitch).inverse * Ry(yaw).inverse * delta_Vgtransfrom from the global frame to the local frame*********************************************************************************///绕y轴旋转(-imuYawStart),即Ry(yaw).inversefloat x1 = cos(imuYawStart) * imuVeloFromStartXCur - sin(imuYawStart) * imuVeloFromStartZCur;float y1 = imuVeloFromStartYCur;float z1 = sin(imuYawStart) * imuVeloFromStartXCur + cos(imuYawStart) * imuVeloFromStartZCur;//绕x轴旋转(-imuPitchStart),即Rx(pitch).inversefloat x2 = x1;float y2 = cos(imuPitchStart) * y1 + sin(imuPitchStart) * z1;float z2 = -sin(imuPitchStart) * y1 + cos(imuPitchStart) * z1;//绕z轴旋转(-imuRollStart),即Rz(pitch).inverseimuVeloFromStartXCur = cos(imuRollStart) * x2 + sin(imuRollStart) * y2;imuVeloFromStartYCur = -sin(imuRollStart) * x2 + cos(imuRollStart) * y2;imuVeloFromStartZCur = z2;
}

接下来就是TransformToStartIMU(PointType *p)函数作用是将当前点先转换到世界坐标系下然后再由世界坐标转换到点云起始点坐标系下。 然后减去加减速造成的非匀速畸变的值。

//去除点云加减速产生的位移畸变
void TransformToStartIMU(PointType *p)
{/********************************************************************************Ry*Rx*Rz*Pl, transform point to the global frame*********************************************************************************///绕z轴旋转(imuRollCur)float x1 = cos(imuRollCur) * p->x - sin(imuRollCur) * p->y;float y1 = sin(imuRollCur) * p->x + cos(imuRollCur) * p->y;float z1 = p->z;//绕x轴旋转(imuPitchCur)float x2 = x1;float y2 = cos(imuPitchCur) * y1 - sin(imuPitchCur) * z1;float z2 = sin(imuPitchCur) * y1 + cos(imuPitchCur) * z1;//绕y轴旋转(imuYawCur)float x3 = cos(imuYawCur) * x2 + sin(imuYawCur) * z2;float y3 = y2;float z3 = -sin(imuYawCur) * x2 + cos(imuYawCur) * z2;/********************************************************************************Rz(pitch).inverse * Rx(pitch).inverse * Ry(yaw).inverse * Pgtransfrom global points to the local frame*********************************************************************************///绕y轴旋转(-imuYawStart)float x4 = cos(imuYawStart) * x3 - sin(imuYawStart) * z3;float y4 = y3;float z4 = sin(imuYawStart) * x3 + cos(imuYawStart) * z3;//绕x轴旋转(-imuPitchStart)float x5 = x4;float y5 = cos(imuPitchStart) * y4 + sin(imuPitchStart) * z4;float z5 = -sin(imuPitchStart) * y4 + cos(imuPitchStart) * z4;//绕z轴旋转(-imuRollStart),然后叠加平移量p->x = cos(imuRollStart) * x5 + sin(imuRollStart) * y5 + imuShiftFromStartXCur;p->y = -sin(imuRollStart) * x5 + cos(imuRollStart) * y5 + imuShiftFromStartYCur;p->z = z5 + imuShiftFromStartZCur;
}

这一部分主要引用:https://blog.csdn.net/liuyanpeng12333/article/details/82737181中讲解IMU进行运动畸变的部分

2.特征点的提取
LOAM中提取的特征点有两种:Edge Point和Planar Point。两种特征使用曲率来进行区分。曲率最大的为Edge Point,曲率最小的为Planar Point。论文中计算曲率的公式如下:
在这里插入图片描述
我对该公式的理解是:
在点云 i i i的左边和右边分别选取几个点。如下图中所示的 k k k点和 j j j点。
在这里插入图片描述
其中向量 i g ig ig的模长就表征了曲率的大小。代码中分别在 i i i点的左侧和右侧选取了5个点来进行曲率计算。

  for (int i = 5; i < cloudSize - 5; i++) {//使用每个点的前后五个点计算曲率,因此前五个与最后五个点跳过float diffX = laserCloud->points[i - 5].x + laserCloud->points[i - 4].x + laserCloud->points[i - 3].x + laserCloud->points[i - 2].x + laserCloud->points[i - 1].x - 10 * laserCloud->points[i].x + laserCloud->points[i + 1].x + laserCloud->points[i + 2].x+ laserCloud->points[i + 3].x + laserCloud->points[i + 4].x+ laserCloud->points[i + 5].x;float diffY = laserCloud->points[i - 5].y + laserCloud->points[i - 4].y + laserCloud->points[i - 3].y + laserCloud->points[i - 2].y + laserCloud->points[i - 1].y - 10 * laserCloud->points[i].y + laserCloud->points[i + 1].y + laserCloud->points[i + 2].y+ laserCloud->points[i + 3].y + laserCloud->points[i + 4].y+ laserCloud->points[i + 5].y;float diffZ = laserCloud->points[i - 5].z + laserCloud->points[i - 4].z + laserCloud->points[i - 3].z + laserCloud->points[i - 2].z + laserCloud->points[i - 1].z - 10 * laserCloud->points[i].z + laserCloud->points[i + 1].z + laserCloud->points[i + 2].z+ laserCloud->points[i + 3].z + laserCloud->points[i + 4].z+ laserCloud->points[i + 5].z;//曲率计算cloudCurvature[i] = diffX * diffX + diffY * diffY + diffZ * diffZ;

3.去除一些不可靠的点。
主要是下面两种情况。
在这里插入图片描述
针对激光光束入射角与障碍物表面夹角小的点,即a图所示的情况,代码中是按下图所示方法来去除的。
在这里插入图片描述
A A A B B B为距离大于阈值(代码里为0.1米)的两个相邻点。其与激光器 O O O的连线就是该点的距离值。找到 A O AO AO连线上的一点 B ′ B' B使得 B O = B ′ O BO=B'O BO=BO。因为弧长除以边长就是夹角 θ \theta θ的弧度值。所以 θ = B B ′ / B O \theta=BB'/BO θ=BB/BO θ \theta θ值越小表示 A B AB AB两点的连线越陡。当 θ \theta θ值小于某一个阈值就认为该点是不可靠的。并且将前方或后方的5个点也置成不可选的点(即不会选为特征点)。下面是该部分的代码实现。

//挑选点,排除容易被斜面挡住的点以及离群点,有些点容易被斜面挡住,而离群点可能出现带有偶然性,这些情况都可能导致前后两次扫描不能被同时看到for (int i = 5; i < cloudSize - 6; i++) {//与后一个点差值,所以减6float diffX = laserCloud->points[i + 1].x - laserCloud->points[i].x;float diffY = laserCloud->points[i + 1].y - laserCloud->points[i].y;float diffZ = laserCloud->points[i + 1].z - laserCloud->points[i].z;//计算有效曲率点与后一个点之间的距离平方和float diff = diffX * diffX + diffY * diffY + diffZ * diffZ;if (diff > 0.1) {//前提:两个点之间距离要大于0.1//点的深度float depth1 = sqrt(laserCloud->points[i].x * laserCloud->points[i].x + laserCloud->points[i].y * laserCloud->points[i].y +laserCloud->points[i].z * laserCloud->points[i].z);//后一个点的深度float depth2 = sqrt(laserCloud->points[i + 1].x * laserCloud->points[i + 1].x + laserCloud->points[i + 1].y * laserCloud->points[i + 1].y +laserCloud->points[i + 1].z * laserCloud->points[i + 1].z);//按照两点的深度的比例,将深度较大的点拉回后计算距离if (depth1 > depth2) {diffX = laserCloud->points[i + 1].x - laserCloud->points[i].x * depth2 / depth1;diffY = laserCloud->points[i + 1].y - laserCloud->points[i].y * depth2 / depth1;diffZ = laserCloud->points[i + 1].z - laserCloud->points[i].z * depth2 / depth1;//边长比也即是弧度值,若小于0.1,说明夹角比较小,斜面比较陡峭,点深度变化比较剧烈,点处在近似与激光束平行的斜面上if (sqrt(diffX * diffX + diffY * diffY + diffZ * diffZ) / depth2 < 0.1) {//排除容易被斜面挡住的点//该点及前面五个点(大致都在斜面上)全部置为筛选过cloudNeighborPicked[i - 5] = 1;//赋成1表示不再考虑为特征点cloudNeighborPicked[i - 4] = 1;cloudNeighborPicked[i - 3] = 1;cloudNeighborPicked[i - 2] = 1;cloudNeighborPicked[i - 1] = 1;cloudNeighborPicked[i] = 1;}} else {diffX = laserCloud->points[i + 1].x * depth1 / depth2 - laserCloud->points[i].x;diffY = laserCloud->points[i + 1].y * depth1 / depth2 - laserCloud->points[i].y;diffZ = laserCloud->points[i + 1].z * depth1 / depth2 - laserCloud->points[i].z;if (sqrt(diffX * diffX + diffY * diffY + diffZ * diffZ) / depth1 < 0.1) {cloudNeighborPicked[i + 1] = 1;cloudNeighborPicked[i + 2] = 1;cloudNeighborPicked[i + 3] = 1;cloudNeighborPicked[i + 4] = 1;cloudNeighborPicked[i + 5] = 1;cloudNeighborPicked[i + 6] = 1;}}}

针对前后遮挡的情况,如b所示,是通过计算前后两点间距的平方是否大于该点距离值平方的万分之二来排除的。下面是代码实现。

    float diffX2 = laserCloud->points[i].x - laserCloud->points[i - 1].x;float diffY2 = laserCloud->points[i].y - laserCloud->points[i - 1].y;float diffZ2 = laserCloud->points[i].z - laserCloud->points[i - 1].z;//与前一个点的距离平方和float diff2 = diffX2 * diffX2 + diffY2 * diffY2 + diffZ2 * diffZ2;//点深度的平方和float dis = laserCloud->points[i].x * laserCloud->points[i].x+ laserCloud->points[i].y * laserCloud->points[i].y+ laserCloud->points[i].z * laserCloud->points[i].z;//与前后点的平方和都大于深度平方和的万分之二,这些点视为离群点,包括陡斜面上的点,强烈凸凹点和空旷区域中的某些点,置为筛选过,弃用if (diff > 0.0002 * dis && diff2 > 0.0002 * dis) {cloudNeighborPicked[i] = 1;}

遍历点云计算好曲率后会对曲率进行从小到大的排序。为了保证特征点的均匀获取,会将点云分成6个部分,每个部分选取一定数量的特征点。

Lidar Odometry
1.激光帧的时间同步
在这里插入图片描述
一帧激光帧经历的时间比较长。一个sweep间隔里,激光点云数据会由于激光器的运动产生畸变。所以作者将前后两帧激光数据都转换到同一时刻,然后再进行激光点云匹配。如上图所示,将完整的一帧激光点云 P k P_k Pk投影到 t t + 1 t_{t+1} tt+1时刻。投影过程会去除点云的运动畸变。在上一个模块(Point Cloud Registration)中,已经用IMU的数据去除了非匀速运动的畸变。在本模块(Lidar Odometr)中,使用上一模块中得到的一个sweep内的平均速度去除匀速运动引起的畸变。最终得到的效果就好像,激光器在 t t + 1 t_{t+1} tt+1时刻静止扫出来的点云。对于新接受的数据 P k + 1 P_{k+1} Pk+1使用同样的方法投影到 t t + 2 t_{t+2} tt+2时刻。注意这里的投影过程是为了后面的点云匹配。所以只进行了特征点云的投影。而当匹配结束后会将当前帧的特征点云和完整的点云数据都投影到该帧最后一个时刻。

2.点云匹配与误差函数

在这里插入图片描述
对于Edge Point,是寻找离当前帧的Edge Point最近的上一帧里的两个Edge Point。同时需要保证这两个Edge Point不能在同一层激光里,正如上图(a)中的点 l l l j j j分别处于不同的层。而误差函数就是描述当前帧的 i i i点到上一帧中 l l l j j j连线的距离。方程公式如下:

在这里插入图片描述
上图的公式表示点到直线的距离。在二维空间中,向量 a a a叉乘向量 b b b的模长等于由向量a和向量b构成的平行四边形的面积。
在这里插入图片描述
在这里插入图片描述

图片引用于https://huangwang.github.io/2019/01/05/%E5%90%91%E9%87%8F%E7%82%B9%E7%A7%AF%E5%8F%89%E7%A7%AF%E5%8F%8A%E5%85%B6%E5%87%A0%E4%BD%95%E6%84%8F%E4%B9%89/

方程中的分子相当于向量 a a a和向量 b b b围成的平行四边形面积。分母就是向量 a b ab ab的模长。平行四边形的面积除以边长 a b ab ab就是向量 a a a和向量 b b b共同端点到直线 a b ab ab的距离。
在这里插入图片描述

对于Planar Point,如Fig7图中(b)所示,找到离当前帧中点 i i i最近的上一帧的3个Planar Point。需要保证这3个Planar Point不在同一层中。这样3个点就形成了一个面。而误差函数就是描述当前帧的 i i i点到上一帧中 l l l j j j m m m点所在平面的距离。方程公式如下:
在这里插入图片描述
在三维几何中,向量a和向量b的叉乘结果是一个向量,有个更通俗易懂的叫法是法向量,该向量垂直于a和b向量构成的平面。方程中点乘的下面那部分会得到单位化的法向量。而向量 i j ij ij点乘单位法向量就相当于向量 i j ij ij在法向量上的投影,即点 i i i到平面的距离。

最终的误差函数是两项误差的和。最后用LM方法迭代(代码中最大迭代25次)求解出前后两帧激光的位姿差。

Lidar Mapping
在这里插入图片描述
上图中, Q k Q_{k} Qk表示到第 k k k次sweep为止累积的点云地图。 Q ˉ k + 1 \bar{Q}_{k+1} Qˉk+1表示投影到map坐标系下的第 k k k次sweep的激光数据。 T k W \boldsymbol{T}_{k}^{W} TkW表示map坐标系下激光器的位姿。 T k + 1 L \boldsymbol{T}_{k+1}^{L} Tk+1L表示 T k W \boldsymbol{T}_{k}^{W} TkW坐标系下激光器的位姿。

从Lidar Odometry模块中,我们可以得到一帧已经去除畸变的激光点云数据和相对于前一帧的粗糙位姿变换。Lidar Mapping模块的任务就是用 Q ˉ k + 1 \bar{Q}_{k+1} Qˉk+1与地图 Q k Q_{k} Qk进行匹配得到一个更为精确的帧间位姿变换,然后建图,为下次匹配做好准备。这里使用的匹配方法与Lidar Odometry模块中是一致的。不同的地方在于这里提取的特征点数量是Lidar Odometry模块的10倍。特征点的匹配不是找对应的2个或者3个特征点。而是相对于当前帧,在地图 Q k Q_{k} Qk中对应位置附近10m10m10m的Cubic中提取所有的特征点。筛选出最近的5个特征点,然后计算协方差。对协方差进行特征值分解,最大特征值对应的向量就是需要匹配的Edge line方向向量,最小特征值对应的向量就是需要匹配的Planar patch方向向量。分别在Edge line上选取两个点,在Planar patch上选取三个点,按照点到线的距离公式和点到面的距离公式构建误差方程。同样使用LM方法迭代(最多迭代10次)求解,得到一个更为精确的帧间位姿变换。

Transform Integration
Transform Integration模块主要是融合了Lidar Mapping得到的位姿变换和Lidar Odometry得到的位姿变换。最终发布一个频率与Lidar Odometry发布频率一致的位姿变换。

LOAM的相关材料

LOAM_velodyne学习该专栏对LOAM代码的的4个模块都做了分析
LOAM 论文及原理分析这篇文章对论文内容做了较为详细的解释
LOAM SLAM代码解析之一:scanRegistration.cpp 点云及IMU数据处理节点这篇文章对IMU数据处理和特征点选取的代码作了分析解释
收集的材料放到自己的github上了:https://github.com/shoufei403/loam_leanring.git

针对LOAM和A-LOAM在KITT上排名差别很大的几点看法

1.LOAM认为一帧激光点云的间隔时间内激光器是匀加减速运动的,并且在点云畸变去除时使用了IMU数据来去除非匀速部分的畸变,然后在Lidar Mapping模块中还利用IMU数据求出平均速度来去除匀速运动引起的畸变。而A-LOAM只认为一帧激光点云的间隔时间内激光器是匀速运动的。然后直接根据时间进行线性插值来进行点云运动畸变去除。很显然LOAM对激光器的运动描述的更为准确,所以效果会更好。

2.A-LOAM使用ceres进行非线性优化求解。而LOAM是作者手写的非线性求解器。这一部分我看不太懂,但猜测代码的工程实现上会有优化,代码执行效率可能更高。

关注公众号《首飞》回复“机器人”获取精心推荐的C/C++,Python,Docker,Qt,ROS1/2,机器人学等机器人行业常用技术资料。


http://chatgpt.dhexx.cn/article/bmo9br2d.shtml

相关文章

LOAM论文阅读

LOAM是Ji Zhang提出的使用激光雷达进行实时建图的算法&#xff0c;即Lidar Odometry and Mapping in Real-time。 Abstract 提出了一个用移动在6自由度的两轴激光雷达实现实时的里程计和建图的算法。由于距离测量值的不同时性和运动估计时产生的畸变导致点云错误匹配。这里作…

LOAM笔记及A-LOAM源码阅读

转载出处&#xff1a;LOAM笔记及A-LOAM源码阅读 - WellP.C - 博客园 导读 下面是我对LOAM论文的理解以及对A-LOAM的源码阅读&#xff08;中文注释版的A-LOAM已经push到github&#xff0c;见A-LOAM-NOTED&#xff09;&#xff0c;最后也会手推一下LOAM源码中高斯牛顿法&#xf…

LOAM算法详解

激光SLAM 帧间匹配方法&#xff1a; Point-to-Plane ICPNDTFeature-based Method 回环检测方法&#xff1a; Scan-to-ScanScan-to-Map LOAM创新点 定位和建图的分离&#xff1a; 里程计模块&#xff1a;高频低质量的帧间运动估计建图模块&#xff1a;低频高质量的用于点云…

loam 框架流程描述

前端流程(scanRegistration.cpp) 多线激光雷达即有多个激光发射器同时工作&#xff0c;如常见的 Velodyne16,就是共有 16 个激光发射器&#xff0c;一般这些发射器竖排排列&#xff0c;然后一起水平旋转。 激光雷达在一定的时间内旋转一圈&#xff0c;即一帧的点云数据。值得注…

LOAM 梳理

LOAM 梳理 本篇文章对 LOAM 框架进行宏观的解析&#xff0c;暂不涉及内部具体的实现原理。 整个流程 整个 LOAM 框架分为三个主要部分&#xff0c;分别为 Registration&#xff08;特征提取&#xff09;、Odometry&#xff08;特征匹配&#xff09;和 Mapping&#xff08;建…

LOAM 原理及代码实现介绍

文章目录 LOAM 原理及代码实现介绍LOAM技术点LOAM整体框架退化问题代码结构 LOAM 原理及代码实现介绍 paper:《Lidar Odometry and Mapping in Real-time》 LOAM的参考代码链接&#xff1a; A-LOAM A-LOAM-Notes LOAM-notes 使用传感器介绍&#xff1a; 如果没有电机旋转&am…

学习LOAM笔记——特征点提取与匹配

学习LOAM笔记——特征点提取与匹配 学习LOAM笔记——特征点提取与匹配1. 特征点提取1.1 对激光点按线束分类1.2 计算激光点曲率1.3 根据曲率提取特征点 2. 特征点匹配2.1 scan-to-scan中的特征点匹配2.2 scan-to-map中特征点匹配 3. 补充 学习LOAM笔记——特征点提取与匹配 兜…

Loam算法详解(配合开源代码aloam)

参考论文&#xff1a;LOAM: Lidar Odometry and Mapping in Real-time 代码&#xff1a;A-LOAM Ubuntu 18.04 ROS Melodic eigen 3.3.4 PCL 1.8.1 ceres 2.0 A-LOAM 配置 LOAM算法是激光slam中一个经典的开源框架&#xff0c;结合qin tong 博士的开源代码a-loam&#xff…

无人驾驶学习笔记 - LOAM 算法论文核心关键点总结

目录 1、框架 2、特征点提取 3、点云去畸变 4、帧间匹配 特征关联与损失函数计算 a 线特征 b 面特征 5、运动估计 6、建图 7、姿态融合 8、LOAM 优劣势 9、参考连接 1、框架 loam框架核心是两部分&#xff0c;高频率的里程计和低频率的建图。两种算法处理。里程计通…

LOAM简介

LOAM 文章目录 LOAMLOAM系统流程1、特征提取&#xff08;1&#xff09;按线数分割&#xff08;2&#xff09;计算曲率&#xff08;3&#xff09;删除异常点&#xff08;4&#xff09;按曲率大小筛选特征点 2、帧间匹配&#xff08;1&#xff09;特征关联与损失函数计算&#xf…

SLAM算法 -LOAM框架分析(一)

LOAM框架分析 算法简介1 激光里程计(高频率)1.1 计算曲率1.2 筛选特征1.3 问题建模1.3.1 损失函数1.3.2 运动补偿 1.4 优化求解1.5 定位输出 2 环境建图(低频率)2.1 问题模型2.2 PCA特征 参考文献 算法简介 LOAM的整体思想就是将复杂的SLAM问题分为&#xff1a;1. 高频的运动估…

顶级高手常用的16个思维模型

“养成一个掌握多元思维模型的习惯是你能做的最有用的事情”&#xff0c;投资家、巴菲特的黄金拍档查理 芒格认为&#xff1a;“思维模型是你大脑中做决策的工具箱。你的工具箱越多&#xff0c;你就越能做出最正确的决策。”与您分享查理芒格的12种思维模型&#xff0c;对做决…

100种思维模型之大脑系统思维模型-52

上世纪60年代&#xff0c;美国神经学家保罗 D 麦克莱恩&#xff08;Paul D. MacLean&#xff09;首次提出“三脑一体”假说&#xff0c;他认为人的大脑是蜥蜴-松鼠-猴子合体的隐喻&#xff0c;代表了进化发展不同阶段的遗传。 复旦大学管理学院的项保华教授对此有一个精彩的总结…

思维模型 SWOT分析

本系列文章 主要是 分享 思维模型&#xff0c;涉及各个领域&#xff0c;重在提升认知 1 模型故事 一个技术岗位想转产品经理&#xff08;个人职业规划SWOT的应用&#xff09;&#xff0c;做的SWOT分析&#xff0c;进而可以量化自己转型的成本&#xff1a; 小王想在自己的居住小…

【转载】100个思维模型(不一定都适用,各取所需)

世界上最有智慧的人是怎样理性思考的&#xff1f;他们在商业活动和个人生活中是如何做决策的&#xff1f; 95岁的智慧老人查理芒格的“多元思维模型”&#xff0c;相信大家都不陌生。 他提倡要学习在所有学科中真正重要的理论&#xff0c;并在此基础上形成所谓的“普世智慧”&…

思维模型 GROW

本系列文章 主要是 分享 思维模型&#xff0c;涉及各个领域&#xff0c;重在提升认知 1 模型故事 推广故事&#xff1a;近期A公司开发一个产品活动的 促销方案&#xff0c;进行了简单的业务流程梳理后便开始了小范围的推广。首先让公司里的同事每人每天拉取20个新人。经过10天…

100种思维模型之细节效率思维模型-74

提及细节效率&#xff1f;也许很多人会有疑问&#xff0c;“效率”怎么跟“细节”挂上钩&#xff0c;注重“细节”了&#xff0c;还能有“高率”&#xff1f; 是的&#xff0c;细节能提高效率&#xff0c;注意某些细节&#xff0c;效率事半功倍。 01、何谓细节效率思维模型 一…

100种思维模型之长远思考思维模型-63

古语有云&#xff1a;“人无远虑&#xff0c;必有近忧&#xff01;” 任正非说&#xff1a;不谋长远者&#xff0c;不足以谋一时&#xff01; 长远思考思维&#xff0c;一个提醒我们要运用长远眼光&#xff0c;树立宏大目标&#xff0c;关注长期利益的思维模型 01何谓长远思考…

提升自己的认知-思维模型

之前写过有关思维的博客思维方式与认知,关于思维方式与做事方式有所思考。最近看到如何判断一个人是杰出的聪明人还是平庸的普通人?的文章,感触颇深。现摘录有所感触的思维方式。 一、思维模型 1、每周“5小时”学习法则 参考:https://36kr.com/p/5130501.html?from=aut…

100种思维模型之坏模因思维模型-44

啥是模因&#xff1f; 简单说&#xff0c;就是 文化基因&#xff0c; 也就是别人传输给我们的 观念。 我们生活中到处都是 模因&#xff0c; 比如&#xff0c;“跟我不一致的观点都是错的”&#xff0c;属于敌对型模因&#xff0c;让别的观念没办法进入你的大脑&#xf…