【3D激光SLAM】LOAM源代码解析--transformMaintenance.cpp
系列文章目录
·【3D激光SLAM】LOAM源代码解析–scanRegistration.cpp
·【3D激光SLAM】LOAM源代码解析–laserOdometry.cpp
·【3D激光SLAM】LOAM源代码解析–laserMapping.cpp
·【3D激光SLAM】LOAM源代码解析–transformMaintenance.cpp
写在前面
本系列文章将对LOAM源代码进行讲解,在讲解过程中,涉及到论文中提到的部分,会结合论文以及我自己的理解进行解读,尤其是对于其中坐标变换的部分,将会进行详细的讲解。
本来是懒得写的,一个是怕自己以后忘了,另外是我在学习过程中,其实没有感觉哪一个博主能讲解的通篇都能让我很明白,特别是坐标变换部分的代码,所以想着自己学完之后,按照自己的理解,也写一个LOAM解读,希望能对后续学习LOAM的同学们有所帮助。
之后也打算录一个LOAM讲解的视频,大家可以关注一下。
文章目录
- 系列文章目录
- 写在前面
- 整体框架
- 一、变量含义
- 二、main()函数
- 三、接收laserMapping的转换信息
- 四、接收laserOdometry的信息
- 五、位姿融合
- 总结
整体框架
LOAM多牛逼就不用多说了,直接开始
先贴一下我详细注释的LOAM代码,在这个版本的代码上加入了我自己的理解。
我觉得最重要也是最恶心的一部分是其中的坐标变换,在代码里面真的看着头大,所以先明确一下坐标系(都是右手坐标系):
- IMU(IMU坐标系imu):x轴向前,y轴向左,z轴向上
- LIDAR(激光雷达坐标系l):x轴向前,y轴向左,z轴向上
- CAMERA(相机坐标系,也可以理解为里程计坐标系c):z轴向前,x轴向左,y轴向上
- WORLD(世界坐标系w,也叫全局坐标系,与里程计第一帧init重合):z轴向前,x轴向左,y轴向上
- MAP(地图坐标系map,一定程度上可以理解为里程计第一帧init):z轴向前,x轴向左,y轴向上
坐标变换约定: 为了清晰,变换矩阵的形式与《SLAM十四讲中一样》,即: R A _ B R_{A\_B} RA_B表示B坐标系相对于A坐标系的变换,B中一个向量通过 R A _ B R_{A\_B} RA_B可以变换到A中的向量。
首先对照ros的节点图和论文中提到的算法框架来看一下:
可以看到节点图和论文中的框架是一一对应的,这几个模块的功能如下:
- scanRegistration:对原始点云进行预处理,计算曲率,提取特征点
- laserOdometry:对当前sweep与上一次sweep进行特征匹配,计算一个快速(10Hz)但粗略的位姿估计
- laserMapping:对当前sweep与一个局部子图进行特征匹配,计算一个慢速(1Hz)比较精确的位姿估计
- transformMaintenance:对两个模块计算出的位姿进行融合,得到最终的精确地位姿估计
本文介绍transformMaintenance模块,它就是将laserOdometry和laserMapping两个模块优化得到的当前帧相对于初始帧的坐标变换进行融合,从而得到最终的最优的坐标变换结果。
一、变量含义
首先,介绍一下本程序用到变量的含义,与laserMapping一致:
- transformBefMapped[6]:从laserMapping模块接收到的,优化前的当前帧相对于初始时刻的位姿变换 T i n i t _ e n d T_{init\_end} Tinit_end
- transformSum[6]:从laserOdometry模块接收到的,当前帧相对于初始时刻的变换 T i n i t _ s t a r t T_{init\_start} Tinit_start
- transformAftMapped[6]:经过laserMapping模块优化后的,当前帧相对于初始时刻的位姿变换 T m a p _ e n d T_{map\_end} Tmap_end
- transformMapped[6]:融合后的当前帧相对于初始帧的坐标变换
一些理解:虽然transformAftMapped[6]我上面写的是 T m a p _ e n d T_{map\_end} Tmap_end,看起来好像是把坐标系换成了map坐标系,但是我觉得这里有两种理解都可以:
- AftMapped可以理解为经过laserMapping模块优化后的里程计坐标系下的当前帧end相对于初始帧的坐标变换
- 也可以理解为经过laserMapping模块优化,变到了map坐标系
二、main()函数
main函数依然很简单,就是定义了一些订阅者和发布者,接收/laser_odom_to_init和/aft_mapped_to_init两个坐标变换话题,然后进入相应的回调函数进行融合;然后发布融合后的当前帧相当于初始帧的坐标变换,以及坐标变换。
int main(int argc, char** argv)
{ros::init(argc, argv, "transformMaintenance");ros::NodeHandle nh;ros::Subscriber subLaserOdometry = nh.subscribe<nav_msgs::Odometry> ("/laser_odom_to_init", 5, laserOdometryHandler);ros::Subscriber subOdomAftMapped = nh.subscribe<nav_msgs::Odometry> ("/aft_mapped_to_init", 5, odomAftMappedHandler);ros::Publisher pubLaserOdometry2 = nh.advertise<nav_msgs::Odometry> ("/integrated_to_init", 5);pubLaserOdometry2Pointer = &pubLaserOdometry2;laserOdometry2.header.frame_id = "/camera_init";laserOdometry2.child_frame_id = "/camera";tf::TransformBroadcaster tfBroadcaster2;tfBroadcaster2Pointer = &tfBroadcaster2;laserOdometryTrans2.frame_id_ = "/camera_init";laserOdometryTrans2.child_frame_id_ = "/camera";ros::spin();return 0;
}
三、接收laserMapping的转换信息
接收/aft_mapped_to_init话题的回调函数很简单,就是将接收到的数据,赋值给transformAftMapped[6]和transformBefMapped[6]变量,这两个变量的含义与laserMapping中一致,就不过多解释了。
//接收laserMapping的转换信息
void odomAftMappedHandler(const nav_msgs::Odometry::ConstPtr& odomAftMapped)
{double roll, pitch, yaw;geometry_msgs::Quaternion geoQuat = odomAftMapped->pose.pose.orientation;tf::Matrix3x3(tf::Quaternion(geoQuat.z, -geoQuat.x, -geoQuat.y, geoQuat.w)).getRPY(roll, pitch, yaw);transformAftMapped[0] = -pitch;transformAftMapped[1] = -yaw;transformAftMapped[2] = roll;transformAftMapped[3] = odomAftMapped->pose.pose.position.x;transformAftMapped[4] = odomAftMapped->pose.pose.position.y;transformAftMapped[5] = odomAftMapped->pose.pose.position.z;transformBefMapped[0] = odomAftMapped->twist.twist.angular.x;transformBefMapped[1] = odomAftMapped->twist.twist.angular.y;transformBefMapped[2] = odomAftMapped->twist.twist.angular.z;transformBefMapped[3] = odomAftMapped->twist.twist.linear.x;transformBefMapped[4] = odomAftMapped->twist.twist.linear.y;transformBefMapped[5] = odomAftMapped->twist.twist.linear.z;
}
四、接收laserOdometry的信息
这个回调函数主要是接收到/laser_odom_to_init话题后进行,先根据接收到的数据对相关变量进行赋值操作,然后进入到transformAssociateToMap()函数进行位姿变换融合,最后将融合后的位姿变换发布出去,发布的话题为:
- /integrated_to_init:融合后的当前帧相对于初始帧(世界坐标系)的位姿变换
另外,广播了/camera相对于/camera_init的坐标变换
//接收laserOdometry的信息
void laserOdometryHandler(const nav_msgs::Odometry::ConstPtr& laserOdometry)
{double roll, pitch, yaw;geometry_msgs::Quaternion geoQuat = laserOdometry->pose.pose.orientation;tf::Matrix3x3(tf::Quaternion(geoQuat.z, -geoQuat.x, -geoQuat.y, geoQuat.w)).getRPY(roll, pitch, yaw);//得到旋转平移矩阵transformSum[0] = -pitch;transformSum[1] = -yaw;transformSum[2] = roll;transformSum[3] = laserOdometry->pose.pose.position.x;transformSum[4] = laserOdometry->pose.pose.position.y;transformSum[5] = laserOdometry->pose.pose.position.z;transformAssociateToMap();geoQuat = tf::createQuaternionMsgFromRollPitchYaw(transformMapped[2], -transformMapped[0], -transformMapped[1]);laserOdometry2.header.stamp = laserOdometry->header.stamp;laserOdometry2.pose.pose.orientation.x = -geoQuat.y;laserOdometry2.pose.pose.orientation.y = -geoQuat.z;laserOdometry2.pose.pose.orientation.z = geoQuat.x;laserOdometry2.pose.pose.orientation.w = geoQuat.w;laserOdometry2.pose.pose.position.x = transformMapped[3];laserOdometry2.pose.pose.position.y = transformMapped[4];laserOdometry2.pose.pose.position.z = transformMapped[5];pubLaserOdometry2Pointer->publish(laserOdometry2);//发送旋转平移量laserOdometryTrans2.stamp_ = laserOdometry->header.stamp;laserOdometryTrans2.setRotation(tf::Quaternion(-geoQuat.y, -geoQuat.z, geoQuat.x, geoQuat.w));laserOdometryTrans2.setOrigin(tf::Vector3(transformMapped[3], transformMapped[4], transformMapped[5]));tfBroadcaster2Pointer->sendTransform(laserOdometryTrans2);
}
五、位姿融合
这里的位姿融合部分与laserMapping中的求解地图坐标系中end时刻到初始时刻的初始猜测–transformAssociateToMap()函数完全一致。
1.求解位移增量
"transformBefMapped - transformSum"的含义是上一帧相对于初始帧的位移量 与 当前帧相对于初始帧的位移量 的差值,得到的结果是初始帧init坐标系下的位移增量 t i n i t s t a r t − e n d t_{init}^{start-end} tinitstart−end。
然后将其变换到end时刻:
t i n i t s t a r t − e n d = R e n d _ i n i t ∗ t i n i t s t a r t − e n d = R i n i t _ e n d − 1 ∗ t i n i t s t a r t − e n d R i n i t _ e n d − 1 = R Z X Y − 1 = R − r z R − r x R − r y t_{init}^{start-end} = R_{end\_init} * t_{init}^{start-end} = R_{init\_end}^{-1} * t_{init}^{start-end} \\ R_{init\_end}^{-1} = R_{ZXY}^{-1} = R_{-rz} R_{-rx} R_{-ry} tinitstart−end=Rend_init∗tinitstart−end=Rinit_end−1∗tinitstart−endRinit_end−1=RZXY−1=R−rzR−rxR−ry
对应于下面代码中所示的变换。
2.求解旋转部分的融合
现在这里的变量含义分别表示为:
- transformSum:laserOdometry模块的当前帧相对于初始帧的变换 R i n i t _ e n d L R_{init\_end}^L Rinit_endL
- transformBefMapped:laserMapping模块的当前帧相对于初始帧的变换 R i n i t _ e n d M R_{init\_end}^M Rinit_endM
- transformAftMapped:laserMapping模块的优化后的当前帧相对于初始帧的变换,也可以理解为当前帧相对于地图坐标系的变换 R m a p _ s t a r t M R_{map\_start}^M Rmap_startM
- transformMapped:融合后的当前帧相对于初始帧的坐标变换 R m a p _ e n d F R_{map\_end}^F Rmap_endF
那么有如下坐标变换关系:
R m a p _ e n d F = R m a p _ e n d M ∗ R i n i t _ e n d M − 1 ∗ R i n i t _ e n d L = R Z X Y ∗ R Z X Y − 1 ∗ R Z X Y R_{map\_end}^F = R_{map\_end}^M * R_{init\_end}^{M -1} * R_{init\_end}^L = R_{ZXY} * R_{ZXY}^{-1} * R_{ZXY} Rmap_endF=Rmap_endM∗Rinit_endM−1∗Rinit_endL=RZXY∗RZXY−1∗RZXY
这里的计算公式与laserOdometry模块中的IMU修正部分完全一样:
R m a p _ e n d F = [ c a c y c a c z + s a c x s a c y s a c z c a c y s a c z + s a c x s a c y c a c z c a c x s a c y c a c x s a c z c a c x c a c z − s a c x − s a c y c a c z + s a c x c a c y s a c z s a c y s a c z + s a c x c a c y c a c z c a c x c a c y ] R_{map\_end}^F=\left[ \begin{matrix} cacycacz+sacxsacysacz& cacysacz+sacxsacycacz& cacxsacy\\ cacxsacz& cacxcacz& -sacx\\ -sacycacz+sacxcacysacz& sacysacz+sacxcacycacz& cacxcacy\\ \end{matrix} \right] Rmap_endF= cacycacz+sacxsacysaczcacxsacz−sacycacz+sacxcacysaczcacysacz+sacxsacycaczcacxcaczsacysacz+sacxcacycaczcacxsacy−sacxcacxcacy
R m a p _ e n d M = [ c b c y c b c z + s b c x s b c y s b c z c b c y s b c z + s b c x s b c y c b c z c b c x s b c y c b c x s b c z c b c x c b c z − s b c x − s b c y c b c z + s b c x c b c y s b c z s b c y s b c z + s b c x c b c y c b c z c b c x c b c y ] R_{map\_end}^M=\left[ \begin{matrix} cbcycbcz+sbcxsbcysbcz& cbcysbcz+sbcxsbcycbcz& cbcxsbcy\\ cbcxsbcz& cbcxcbcz& -sbcx\\ -sbcycbcz+sbcxcbcysbcz& sbcysbcz+sbcxcbcycbcz& cbcxcbcy\\ \end{matrix} \right] Rmap_endM= cbcycbcz+sbcxsbcysbczcbcxsbcz−sbcycbcz+sbcxcbcysbczcbcysbcz+sbcxsbcycbczcbcxcbczsbcysbcz+sbcxcbcycbczcbcxsbcy−sbcxcbcxcbcy
R i n i t _ e n d M − 1 = [ c b l y c b l z − s b l x s b l y s b l z − c b l x s b l z s b l y c b l z + s b l x c b l y s b l z − c b l y s b l z + s b l x s b l y c b l z c b l x c b l z s b l y s b l z − s b l x c b l y c b l z − c b l x s b l y s b l x c b l x c b l y ] R_{init\_end}^{M -1}=\left[ \begin{matrix} cblycblz-sblxsblysblz& -cblxsblz& sblycblz+sblxcblysblz\\ -cblysblz+sblxsblycblz& cblxcblz& sblysblz-sblxcblycblz\\ -cblxsbly& sblx& cblxcbly\\ \end{matrix} \right] Rinit_endM−1= cblycblz−sblxsblysblz−cblysblz+sblxsblycblz−cblxsbly−cblxsblzcblxcblzsblxsblycblz+sblxcblysblzsblysblz−sblxcblycblzcblxcbly
R i n i t _ e n d L = [ c a l y c a l z + s a l x s a l y s a l z c a l y s a l z + s a l x s a l y c a l z c a l x s a l y c a l x s a l z c a l x c a l z − s a l x − s a l y c a l z + s a l x c a l y s a l z s a l y s a l z + s a l x c a l y c a l z c a l x c a l y ] R_{init\_end}^L=\left[ \begin{matrix} calycalz+salxsalysalz& calysalz+salxsalycalz& calxsaly\\ calxsalz& calxcalz& -salx\\ -salycalz+salxcalysalz& salysalz+salxcalycalz& calxcaly\\ \end{matrix} \right] Rinit_endL= calycalz+salxsalysalzcalxsalz−salycalz+salxcalysalzcalysalz+salxsalycalzcalxcalzsalysalz+salxcalycalzcalxsaly−salxcalxcaly
然后使用对应位置的值相等,就得到了修正后的累计变换acx、acy、acz,计算如下:
a c x = − a r c s i n ( R 2 , 3 ) = − a r c s i n ( − s b c x ∗ ( s a l x ∗ s b l x + c a l x ∗ c a l y ∗ c b l x ∗ c b l y + c a l x ∗ c b l x ∗ s a l y ∗ s b l y ) − c b c x ∗ c b c z ∗ ( c a l x ∗ s a l y ∗ ( c b l y ∗ s b l z − c b l z ∗ s b l x ∗ s b l y ) − c a l x ∗ c a l y ∗ ( s b l y ∗ s b l z + c b l y ∗ c b l z ∗ s b l x ) + c b l x ∗ c b l z ∗ s a l x ) − c b c x ∗ s b c z ∗ ( c a l x ∗ c a l y ∗ ( c b l z ∗ s b l y − c b l y ∗ s b l x ∗ s b l z ) − c a l x ∗ s a l y ∗ ( c b l y ∗ c b l z + s b l x ∗ s b l y ∗ s b l z ) + c b l x ∗ s a l x ∗ s b l z ) ) a c y = a r c t a n ( R 1 , 3 / R 3 , 3 ) a c z = a r c t a n ( R 2 , 1 / R 2 , 2 ) acx = -arcsin(R_{2,3}) = -arcsin(-sbcx*(salx*sblx + calx*caly*cblx*cbly + calx*cblx*saly*sbly) - cbcx*cbcz*(calx*saly*(cbly*sblz - cblz*sblx*sbly) - calx*caly*(sbly*sblz + cbly*cblz*sblx) + cblx*cblz*salx) - cbcx*sbcz*(calx*caly*(cblz*sbly - cbly*sblx*sblz) - calx*saly*(cbly*cblz + sblx*sbly*sblz) + cblx*salx*sblz) ) \\ acy = arctan(R_{1,3}/R_{3,3}) \\ acz = arctan(R_{2,1}/R_{2,2}) acx=−arcsin(R2,3)=−arcsin(−sbcx∗(salx∗sblx+calx∗caly∗cblx∗cbly+calx∗cblx∗saly∗sbly)−cbcx∗cbcz∗(calx∗saly∗(cbly∗sblz−cblz∗sblx∗sbly)−calx∗caly∗(sbly∗sblz+cbly∗cblz∗sblx)+cblx∗cblz∗salx)−cbcx∗sbcz∗(calx∗caly∗(cblz∗sbly−cbly∗sblx∗sblz)−calx∗saly∗(cbly∗cblz+sblx∗sbly∗sblz)+cblx∗salx∗sblz))acy=arctan(R1,3/R3,3)acz=arctan(R2,1/R2,2)
3.将位移增量转换到map坐标系
t m a p i n c r e m e n t = R m a p _ e n d F ∗ t e n d i n c r e m e n t R m a p _ e n d F = R Z X Y = R y R x R z t_{map}^{increment} = R_{map\_end}^F * t_{end}^{increment} \\ R_{map\_end}^F = R_{ZXY} = R_y R_x R_z tmapincrement=Rmap_endF∗tendincrementRmap_endF=RZXY=RyRxRz
4.求解平移部分的初始猜测
这里注意一点:上面求出来的增量使用的事start时刻的累积位移减去end时刻的累计位移,所以这里在求解时也是减号,如下:
t m a p _ e n d F = t m a p _ e n d M + t m a p e n d − s t a r t = t m a p _ s t a r t M − t m a p s t a r t − e n d t_{map\_end}^F = t_{map\_end}^M + t_{map}^{end-start} = t_{map\_start}^M - t_{map}^{start-end} tmap_endF=tmap_endM+tmapend−start=tmap_startM−tmapstart−end
我在上面声明变量时提到了:地图坐标系map,一定程度上可以理解为里程计第一帧init,这个意思就是可以理解为map坐标系和初始时刻坐标系init以及世界坐标系w是重合的,而laserMapping中虽然写的是变换到了map坐标系,也可以理解为仍然是当前帧end相对于初始帧init的坐标变换,只是经过了laserMapping模块优化,所以这里的 t m a p _ e n d F t_{map\_end}^F tmap_endF也可以写成 t i n i t _ e n d F t_{init\_end}^F tinit_endF,这个解释只是为了符合作者代码中坐标变换时发布的是/camera_init到/camera的变换,所以这里写 t m a p _ e n d F t_{map\_end}^F tmap_endF也没问题。
//odometry的运动估计和mapping矫正量融合之后得到的最终的位姿transformMapped
void transformAssociateToMap()
{//平移后绕y轴旋转(-transformSum[1])float x1 = cos(transformSum[1]) * (transformBefMapped[3] - transformSum[3]) - sin(transformSum[1]) * (transformBefMapped[5] - transformSum[5]);float y1 = transformBefMapped[4] - transformSum[4];float z1 = sin(transformSum[1]) * (transformBefMapped[3] - transformSum[3]) + cos(transformSum[1]) * (transformBefMapped[5] - transformSum[5]);//绕x轴旋转(-transformSum[0])float x2 = x1;float y2 = cos(transformSum[0]) * y1 + sin(transformSum[0]) * z1;float z2 = -sin(transformSum[0]) * y1 + cos(transformSum[0]) * z1;//绕z轴旋转(-transformSum[2])transformIncre[3] = cos(transformSum[2]) * x2 + sin(transformSum[2]) * y2;transformIncre[4] = -sin(transformSum[2]) * x2 + cos(transformSum[2]) * y2;transformIncre[5] = z2;float sbcx = sin(transformSum[0]);float cbcx = cos(transformSum[0]);float sbcy = sin(transformSum[1]);float cbcy = cos(transformSum[1]);float sbcz = sin(transformSum[2]);float cbcz = cos(transformSum[2]);float sblx = sin(transformBefMapped[0]);float cblx = cos(transformBefMapped[0]);float sbly = sin(transformBefMapped[1]);float cbly = cos(transformBefMapped[1]);float sblz = sin(transformBefMapped[2]);float cblz = cos(transformBefMapped[2]);float salx = sin(transformAftMapped[0]);float calx = cos(transformAftMapped[0]);float saly = sin(transformAftMapped[1]);float caly = cos(transformAftMapped[1]);float salz = sin(transformAftMapped[2]);float calz = cos(transformAftMapped[2]);float srx = -sbcx*(salx*sblx + calx*cblx*salz*sblz + calx*calz*cblx*cblz)- cbcx*sbcy*(calx*calz*(cbly*sblz - cblz*sblx*sbly)- calx*salz*(cbly*cblz + sblx*sbly*sblz) + cblx*salx*sbly)- cbcx*cbcy*(calx*salz*(cblz*sbly - cbly*sblx*sblz) - calx*calz*(sbly*sblz + cbly*cblz*sblx) + cblx*cbly*salx);transformMapped[0] = -asin(srx);float srycrx = sbcx*(cblx*cblz*(caly*salz - calz*salx*saly)- cblx*sblz*(caly*calz + salx*saly*salz) + calx*saly*sblx)- cbcx*cbcy*((caly*calz + salx*saly*salz)*(cblz*sbly - cbly*sblx*sblz)+ (caly*salz - calz*salx*saly)*(sbly*sblz + cbly*cblz*sblx) - calx*cblx*cbly*saly)+ cbcx*sbcy*((caly*calz + salx*saly*salz)*(cbly*cblz + sblx*sbly*sblz)+ (caly*salz - calz*salx*saly)*(cbly*sblz - cblz*sblx*sbly) + calx*cblx*saly*sbly);float crycrx = sbcx*(cblx*sblz*(calz*saly - caly*salx*salz)- cblx*cblz*(saly*salz + caly*calz*salx) + calx*caly*sblx)+ cbcx*cbcy*((saly*salz + caly*calz*salx)*(sbly*sblz + cbly*cblz*sblx)+ (calz*saly - caly*salx*salz)*(cblz*sbly - cbly*sblx*sblz) + calx*caly*cblx*cbly)- cbcx*sbcy*((saly*salz + caly*calz*salx)*(cbly*sblz - cblz*sblx*sbly)+ (calz*saly - caly*salx*salz)*(cbly*cblz + sblx*sbly*sblz) - calx*caly*cblx*sbly);transformMapped[1] = atan2(srycrx / cos(transformMapped[0]), crycrx / cos(transformMapped[0]));float srzcrx = (cbcz*sbcy - cbcy*sbcx*sbcz)*(calx*salz*(cblz*sbly - cbly*sblx*sblz)- calx*calz*(sbly*sblz + cbly*cblz*sblx) + cblx*cbly*salx)- (cbcy*cbcz + sbcx*sbcy*sbcz)*(calx*calz*(cbly*sblz - cblz*sblx*sbly)- calx*salz*(cbly*cblz + sblx*sbly*sblz) + cblx*salx*sbly)+ cbcx*sbcz*(salx*sblx + calx*cblx*salz*sblz + calx*calz*cblx*cblz);float crzcrx = (cbcy*sbcz - cbcz*sbcx*sbcy)*(calx*calz*(cbly*sblz - cblz*sblx*sbly)- calx*salz*(cbly*cblz + sblx*sbly*sblz) + cblx*salx*sbly)- (sbcy*sbcz + cbcy*cbcz*sbcx)*(calx*salz*(cblz*sbly - cbly*sblx*sblz)- calx*calz*(sbly*sblz + cbly*cblz*sblx) + cblx*cbly*salx)+ cbcx*cbcz*(salx*sblx + calx*cblx*salz*sblz + calx*calz*cblx*cblz);transformMapped[2] = atan2(srzcrx / cos(transformMapped[0]), crzcrx / cos(transformMapped[0]));x1 = cos(transformMapped[2]) * transformIncre[3] - sin(transformMapped[2]) * transformIncre[4];y1 = sin(transformMapped[2]) * transformIncre[3] + cos(transformMapped[2]) * transformIncre[4];z1 = transformIncre[5];x2 = x1;y2 = cos(transformMapped[0]) * y1 - sin(transformMapped[0]) * z1;z2 = sin(transformMapped[0]) * y1 + cos(transformMapped[0]) * z1;transformMapped[3] = transformAftMapped[3] - (cos(transformMapped[1]) * x2 + sin(transformMapped[1]) * z2);transformMapped[4] = transformAftMapped[4] - y2;transformMapped[5] = transformAftMapped[5] - (-sin(transformMapped[1]) * x2 + cos(transformMapped[1]) * z2);
}
总结
到此为止,整个LOAM的讲解就结束了!!
我的感觉就是看LOAM的论文,有一种“作者说的好有道理,确实就是这样啊”的感觉,但是如果要是让自己想,就想不出来这么牛逼的算法,它的代码也写的比较漂亮。
代码的运行就不单独开一篇文章写了,只要装好了依赖,编译很顺畅,也没报什么错,我找了一个数据集测试了一下,也没问题,测试的数据里放在了文章开头提到的我的github仓库的bag文件夹中,运行结果点云图放在了pcl文件夹中,放一张结果截图。
相关文章:

【3D激光SLAM】LOAM源代码解析--transformMaintenance.cpp
系列文章目录 【3D激光SLAM】LOAM源代码解析–scanRegistration.cpp 【3D激光SLAM】LOAM源代码解析–laserOdometry.cpp 【3D激光SLAM】LOAM源代码解析–laserMapping.cpp 【3D激光SLAM】LOAM源代码解析–transformMaintenance.cpp 写在前面 本系列文章将对LOAM源代码进行讲解…...

DiscuzQ 二开教程(7)——二次开发版本部署文档
DiscuzQ 二开教程(7)——二次开发版本部署文档 源码:Discuz-Q-V3: 本仓库为Discuz-Q V3.0.211111 版本的二次开发版本,是将DiscuzQ官方仓库进行合并代码(All in One)整理后的仓库,使用更方便。…...

u盘数据丢失但占内存如何恢复?不要着急,这里有拯救方案
U盘数据丢失但占内存如何恢复?数据丢失是一种让人非常头疼的问题,尤其是当我们的U盘数据丢失了,但内存仍然被占用时,更令人困惑和焦虑。然而,不要慌张!在本文中,将为大家介绍一些有效的方法来恢…...
springboot日志文件名称为什么叫logback-spring.xml
如题,为什么springboot日志配置文件叫logback-spring.xml? 在整个项目中搜索 logback-spring.xml 并没有搜索到。 先看一下 org.springframework.boot.context.logging.LoggingApplicationListener#initialize protected void initialize(ConfigurableEn…...

Mysql 开窗函数(窗口函数)
文章目录 全部数据示例1(说明)开窗函数可以比groupby多查出条件列外的字段,开窗函数主要是为了跟聚合函数一起使用,达到分组统计效果,并且开窗函数的结果集基本都是跟总行数一样示例2示例3示例4错误示例1错误示例2错误…...
计算机视觉之图像特征提取
图像特征提取是计算机视觉中的重要任务,它有助于识别、分类、检测和跟踪对象。以下是一些常用的图像特征提取算法及其简介: 颜色直方图(Color Histogram): 简介:颜色直方图表示图像中各种颜色的分布情况。通…...

【面试经典150题】移除元素·JavaScript版
题目来源 大致思路:遍历数组,如果遇到值为val的元素,使用数组最后一个元素替换它。详细过程: /*** param {number[]} nums* param {number} val* return {number}*/ var removeElement function(nums, val) {let i0,nnums.leng…...
Cesium 相机的三种放置方式
文章目录 Cesium 相机的三种放置方式第一种:setView 计算视角1. Cartesian3 方式2. Rectangle 方式 第二种:flyTo第三种:lookAt Cesium 相机的三种放置方式 Cesium 提供了三种方式对相机的位置进行摆放 第一种:setView 计算视角 …...

看了我这篇帖子,你还会觉得制作电子杂志很难吗?
如果你也像我一样笨手笨脚的不会设计排版制作杂志也没关系,用FLBOOK就能在线制作电子杂志,效果极好! 工具:FLBOOK 步骤如下: 1.打开FLBOOK在线制作电子杂志平台 2.点击开始创作,有四个创建作品的方式&…...

SRE 与开发的自动化协同 -- 生产环境出现 bug 自动生成异常追踪
简介 生产环境 bug 的定义:RUM 应用和 APM 应用的 error_stack 信息被捕捉后成为 bug。 以 APM 新增错误巡检为例,当出现新错误时,在观测云控制台的「事件」模块下生成新的事件报告,捕捉为 bug。同时利用 Dataflux Func 创建异常…...

【简单认识Docker基本管理】
文章目录 一、Docker概述1、定义2.容器化流行的原因3.Docker和虚拟机的区别4.Docker核心概念 二、安装docker三、镜像管理1.搜索镜像2.下载(拉取)镜像3.查看已下载镜像4.查看镜像详细信息5.修改镜像标签6.删除镜像7.导出镜像文件和拉取本地镜像文件8.上传…...

设备管理系统是什么?的修设备管理系统有什么功能?
随着计算机技术的迅速发展和移动互联网的商业化和社会化应用,设备的种类和数量急剧增加。如何利用先进的网络技术和快速更新的计算机设备来有效地收集和处理设备信息,建立以信息化为核心的管理体系,减轻管理和业务人员的数据处理负担…...

Docker安装并配置Pushgateway
Linux下安装Docker请参考:Linux安装Docker 简介 Pushgateway是Prometheus的一个组件,prometheus server默认是通过Exporter主动获取数据(默认采取pull拉取数据),Pushgateway则是通过exporter主动方式推送数据到Pushg…...
汽车OTA活动高质量发展的“常”与“新”
伴随着车主的频繁崔更,车企除了卷硬件、拼价格,逐渐将精力转移到汽车全生命周期的常用常新。时至下半年,车企OTA圈愈发热闹,以新势力、新实力为代表新一代车企,OTA运营活动逐渐进入高质量发展期。 所谓高质量…...

C++信息学奥赛1121:计算矩阵边缘元素之和
题解:i0 or j0 or in-1 or jm-1 or in-1 or jm-1 代码: #include<iostream> // 包含输入输出流库 #include<cmath> // 包含数学函数库 using namespace std; // 使用标准命名空间int main() {int n,m;cin>>n>>m; // 输入…...

Android Selector 的使用
什么是 Selector? Selector 和 Shape 相似,是Drawable资源的一种,可以根据不同的状态,设置不同的图片效果,关键节点 < selector > ,例如:我们只需要将Button的 background 属性设置为该dr…...
k8s集群中service的域名解析、pod的域名解析
前言 在k8s集群中,service和pod都可以通过域名的形式进行相互通信,换句话说,在k8s集群内,通过service和pod的域名,可以直接访问内部应用,不必在通过service ip地址进行通信,一般的,…...

Shell 编程快速入门 之 数学计算和函数基础
目录 1. 求两数之和 整数之和 浮点数之和 2. 计算1-100的和 for...in C风格for循环 while...do until...do while和until的区别 关系运算符 break与continue的区别 3. shell函数基础知识 函数定义 函数名 函数体 参数 返回值 return返回值的含义 return与…...
学习php中如何获取pdf文件中的文本内容
学习php中如何获取pdf文件中的文本内容 要使用PHP获取PDF文件中的文本内容,可以使用PDF解析库。以下是一些流行的PDF解析库: pdftotext:它是一个命令行工具,可以将PDF文件转换为文本文件。可以使用PHP exec()函数运行该工具。 FP…...

分布式数据库架构:高可用、高性能的数据存储
在现代信息时代,数据是企业发展的核心。为了支持海量数据的存储、高并发访问以及保证数据的可靠性,分布式数据库架构应运而生。分布式数据库架构是一种将数据存储在多个物理节点上,并通过一系列复杂的协调和管理机制来提供高可用性和高性能的…...

Chapter03-Authentication vulnerabilities
文章目录 1. 身份验证简介1.1 What is authentication1.2 difference between authentication and authorization1.3 身份验证机制失效的原因1.4 身份验证机制失效的影响 2. 基于登录功能的漏洞2.1 密码爆破2.2 用户名枚举2.3 有缺陷的暴力破解防护2.3.1 如果用户登录尝试失败次…...

Flask RESTful 示例
目录 1. 环境准备2. 安装依赖3. 修改main.py4. 运行应用5. API使用示例获取所有任务获取单个任务创建新任务更新任务删除任务 中文乱码问题: 下面创建一个简单的Flask RESTful API示例。首先,我们需要创建环境,安装必要的依赖,然后…...

VB.net复制Ntag213卡写入UID
本示例使用的发卡器:https://item.taobao.com/item.htm?ftt&id615391857885 一、读取旧Ntag卡的UID和数据 Private Sub Button15_Click(sender As Object, e As EventArgs) Handles Button15.Click轻松读卡技术支持:网站:Dim i, j As IntegerDim cardidhex, …...

黑马Mybatis
Mybatis 表现层:页面展示 业务层:逻辑处理 持久层:持久数据化保存 在这里插入图片描述 Mybatis快速入门 方法配合annotate()方法来实现。这里有两种常见的方法来完成这个需求: 方法1:使用annotate()和Count 假设你有一个模型Item,并且你想…...
VTK如何让部分单位不可见
最近遇到一个需求,需要让一个vtkDataSet中的部分单元不可见,查阅了一些资料大概有以下几种方式 1.通过颜色映射表来进行,是最正规的做法 vtkNew<vtkLookupTable> lut; //值为0不显示,主要是最后一个参数,透明度…...

MySQL 8.0 OCP 英文题库解析(十三)
Oracle 为庆祝 MySQL 30 周年,截止到 2025.07.31 之前。所有人均可以免费考取原价245美元的MySQL OCP 认证。 从今天开始,将英文题库免费公布出来,并进行解析,帮助大家在一个月之内轻松通过OCP认证。 本期公布试题111~120 试题1…...

网络编程(UDP编程)
思维导图 UDP基础编程(单播) 1.流程图 服务器:短信的接收方 创建套接字 (socket)-----------------------------------------》有手机指定网络信息-----------------------------------------------》有号码绑定套接字 (bind)--------------…...
Rapidio门铃消息FIFO溢出机制
关于RapidIO门铃消息FIFO的溢出机制及其与中断抖动的关系,以下是深入解析: 门铃FIFO溢出的本质 在RapidIO系统中,门铃消息FIFO是硬件控制器内部的缓冲区,用于临时存储接收到的门铃消息(Doorbell Message)。…...

智能分布式爬虫的数据处理流水线优化:基于深度强化学习的数据质量控制
在数字化浪潮席卷全球的今天,数据已成为企业和研究机构的核心资产。智能分布式爬虫作为高效的数据采集工具,在大规模数据获取中发挥着关键作用。然而,传统的数据处理流水线在面对复杂多变的网络环境和海量异构数据时,常出现数据质…...