lego-loam mapOptmization 源码注释(二)

看过了main函数,我们来看mapOptmization的正题:

MO.run();

void run(){if (newLaserCloudCornerLast  && std::abs(timeLaserCloudCornerLast  - timeLaserOdometry) < 0.005 &&newLaserCloudSurfLast    && std::abs(timeLaserCloudSurfLast    - timeLaserOdometry) < 0.005 &&newLaserCloudOutlierLast && std::abs(timeLaserCloudOutlierLast - timeLaserOdometry) < 0.005 &&newLaserOdometry)
//函数开始时,会检查是否收到了新的激光雷达角点云数据 (newLaserCloudCornerLast)、
表面点云数据 (newLaserCloudSurfLast)、离群点云数据 (newLaserCloudOutlierLast) 
和里程计数据 (newLaserOdometry)。同时,还会检查这些数据的时间戳与最新的里程计数据的
时间戳之间的差异是否小于0.005秒。这是为了确保所有数据都是在相近的时间点采集的,
从而可以认为是同步的。{newLaserCloudCornerLast = false; newLaserCloudSurfLast = false; newLaserCloudOutlierLast = false; newLaserOdometry = false;
//如果所有数据都已就绪,会将所有新数据的标志位重置为 false,表示这些数据已经被处理。std::lock_guard<std::mutex> lock(mtx);
//使用 std::lock_guard 和 std::mutex 来保护共享数据,确保线程安全。
//这是为了防止多个线程同时访问和修改共享数据,可能会导致数据不一致。if (timeLaserOdometry - timeLastProcessing >= mappingProcessInterval) {//extern const double mappingProcessInterval = 0.3;//检查自上次处理以来是否已经过了设定的时间间隔 (mappingProcessInterval)。//这是为了避免过于频繁的处理,从而节省计算资源。timeLastProcessing = timeLaserOdometry;//更新时间,为下一次判断做准备transformAssociateToMap();extractSurroundingKeyFrames();downsampleCurrentScan();scan2MapOptimization();saveKeyFramesAndFactor();correctPoses();publishTF();publishKeyPosesAndFrames();clearCloud();}}}

 下面我们来看第一个函数,transformAssociateToMap();

 transformAssociateToMap();

void transformAssociateToMap(){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]);float x2 = x1;float y2 = cos(transformSum[0]) * y1 + sin(transformSum[0]) * z1;float z2 = -sin(transformSum[0]) * y1 + cos(transformSum[0]) * z1;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);transformTobeMapped[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);transformTobeMapped[1] = atan2(srycrx / cos(transformTobeMapped[0]), crycrx / cos(transformTobeMapped[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);transformTobeMapped[2] = atan2(srzcrx / cos(transformTobeMapped[0]), crzcrx / cos(transformTobeMapped[0]));x1 = cos(transformTobeMapped[2]) * transformIncre[3] - sin(transformTobeMapped[2]) * transformIncre[4];y1 = sin(transformTobeMapped[2]) * transformIncre[3] + cos(transformTobeMapped[2]) * transformIncre[4];z1 = transformIncre[5];x2 = x1;y2 = cos(transformTobeMapped[0]) * y1 - sin(transformTobeMapped[0]) * z1;z2 = sin(transformTobeMapped[0]) * y1 + cos(transformTobeMapped[0]) * z1;transformTobeMapped[3] = transformAftMapped[3] - (cos(transformTobeMapped[1]) * x2 + sin(transformTobeMapped[1]) * z2);transformTobeMapped[4] = transformAftMapped[4] - y2;transformTobeMapped[5] = transformAftMapped[5] - (-sin(transformTobeMapped[1]) * x2 + cos(transformTobeMapped[1]) * z2);}

 我们要尝试理解transformAssociateToMap();就要先理解:transformSum,transformBefMapped,transformAftMapped,transformTobeMapped分别是什么?

transformSum是当前帧雷达里程计的结果,transformBefMapped是上一帧雷达里程计的结果。

这在transformUpdate()函数中有所体现。transformAftMapped是上一帧优化后的位姿,transformTobeMapped是当前帧优化后的位姿。

第一次运行时,除transformSum,其他均初始化为0。

		for (int i = 0; i < 6; i++) {transformBefMapped[i] = transformSum[i];transformAftMapped[i] = transformTobeMapped[i];}

所以要得到当前帧优化后的位姿,就要用上一帧优化后的位姿上一帧雷达里程计结果的逆,再乘当前帧雷达里程计的结果。公式如下:

参考自LOAM中的坐标变化公式推导_loam公式推导-CSDN博客

从xyz1到transformIncre的过程就对应当前帧雷达里程计结果的逆(上一帧雷达里程计的平移量-当前帧雷达里程计的平移量)。公式如下:

我的理解就是,当前的平移量差值△r,是在上一次位姿优化后,由于位姿变换产生的,我们先把它变换回上一次的位姿,也就是左乘 当前帧雷达里程计结果的逆,然后再乘transformTobeMapped,变换到当前帧优化后的位姿上。

        transformSum[0] = -pitch;transformSum[1] = -yaw;transformSum[2] = roll;
//那为什么公式中没有求逆的过程呢?对于旋转矩阵来说,yaw-pitch-roll的逆就是按roll-pitch-yaw顺序旋转。

transformTobeMapped 的公式推导就是矩阵乘法的展开式,类似于PluginIMURotation的公式推导,用恒等关系解决,具体推导见:LOAM中的坐标变化公式推导_loam公式推导-CSDN博客

extractSurroundingKeyFrames();

void extractSurroundingKeyFrames(){if (cloudKeyPoses3D->points.empty() == true)return;	}

 only use recent key poses for graph building只使用临近帧的位姿去建图。

初始的cloudKeyPoses3D就是空的,它的值会在saveKeyFramesAndFactor()函数中更新,所以可以把cloudKeyPoses3D简单理解为上一帧的点云

!!但是其实cloudKeyPoses3D不是点云,它只是一个上一帧机器人的位置,但是这个位置可以代替一帧点云,这样运行时就大大减少了内存使用!!

thisPose3D.x = latestEstimate.translation().y();
thisPose3D.y = latestEstimate.translation().z();
thisPose3D.z = latestEstimate.translation().x();
thisPose3D.intensity = cloudKeyPoses3D->points.size(); // this can be used as index
cloudKeyPoses3D->push_back(thisPose3D);
当 extractSurroundingKeyFrames() 中执行 return 后,run() 函数中的代码会继续执行,并不会因为 extractSurroundingKeyFrames() 返回就中断 run() 的执行。具体来说,extractSurroundingKeyFrames() 函数中如果 cloudKeyPoses3D->points.empty() 为 true,会直接返回,跳过当前函数的其他内容。但因为这是 extractSurroundingKeyFrames() 内部的 return,仅仅是提前结束了该函数的执行,不会影响 run() 中其后的代码执行。所以在 extractSurroundingKeyFrames() 执行 return 后,run() 函数会继续执行 downsampleCurrentScan()、scan2MapOptimization() 等后续函数。

 所以我们按cloudKeyPoses3D就是空的,先看return之后的代码,也就是下一个函数downsampleCurrentScan();。

当cloudKeyPoses3D->points.empty() == false,我们来看下面的代码:

如果需要回环检测:loopClosureEnableFlag == true

void extractSurroundingKeyFrames(){if (cloudKeyPoses3D->points.empty() == true)return;	if (loopClosureEnableFlag == true){
//extern const bool loopClosureEnableFlag = false;
//默认值是false,也就是默认不进行回环检测。// only use recent key poses for graph buildingif (recentCornerCloudKeyFrames.size() < surroundingKeyframeSearchNum){ 
// queue is not full (the beginning of mapping or a loop is just closed)
//extern const int surroundingKeyframeSearchNum = 50; 
// submap size (when loop closure enabled)// clear recent key frames queuerecentCornerCloudKeyFrames. clear();recentSurfCloudKeyFrames.   clear();recentOutlierCloudKeyFrames.clear();int numPoses = cloudKeyPoses3D->points.size();for (int i = numPoses-1; i >= 0; --i){int thisKeyInd = (int)cloudKeyPoses3D->points[i].intensity;PointTypePose thisTransformation = cloudKeyPoses6D->points[thisKeyInd];updateTransformPointCloudSinCos(&thisTransformation);// extract surrounding maprecentCornerCloudKeyFrames. push_front(transformPointCloud(cornerCloudKeyFrames[thisKeyInd]));recentSurfCloudKeyFrames.   push_front(transformPointCloud(surfCloudKeyFrames[thisKeyInd]));recentOutlierCloudKeyFrames.push_front(transformPointCloud(outlierCloudKeyFrames[thisKeyInd]));if (recentCornerCloudKeyFrames.size() >= surroundingKeyframeSearchNum)
//显然当我们刚开始建图的时候,附近的关键帧是不够的,需要积累一段时间后才会break。
//积累达到的条件就是子地图中有50个关键帧。break;}}

如果我们的关键帧满了,pop the oldest key frame and push the latest key frame。

但是如果机器人没有移动,没有新的关键帧产生,这时就不需要pop the oldest key frame and push the latest key frame。也就是latestFrameID == cloudKeyPoses3D->points.size() - 1的情况。

else{  // queue is full, pop the oldest key frame and push the latest key frameif (latestFrameID != cloudKeyPoses3D->points.size() - 1){  // if the robot is not moving, no need to update recent framesrecentCornerCloudKeyFrames. pop_front();recentSurfCloudKeyFrames.   pop_front();recentOutlierCloudKeyFrames.pop_front();// push latest scan to the end of queuelatestFrameID = cloudKeyPoses3D->points.size() - 1;PointTypePose thisTransformation = cloudKeyPoses6D->points[latestFrameID];updateTransformPointCloudSinCos(&thisTransformation);recentCornerCloudKeyFrames. push_back(transformPointCloud(cornerCloudKeyFrames[latestFrameID]));recentSurfCloudKeyFrames.   push_back(transformPointCloud(surfCloudKeyFrames[latestFrameID]));recentOutlierCloudKeyFrames.push_back(transformPointCloud(outlierCloudKeyFrames[latestFrameID]));}}for (int i = 0; i < recentCornerCloudKeyFrames.size(); ++i){*laserCloudCornerFromMap += *recentCornerCloudKeyFrames[i];*laserCloudSurfFromMap   += *recentSurfCloudKeyFrames[i];*laserCloudSurfFromMap   += *recentOutlierCloudKeyFrames[i];//将周围关键帧中的点添加到子地图中。}}

 如果不需要回环检测:loopClosureEnableFlag == false

这种情况是代码中设置的默认情况:也就是loam的做法

else{surroundingKeyPoses->clear();surroundingKeyPosesDS->clear();
//将 surroundingKeyPoses 和 surroundingKeyPosesDS 清空,为提取新的周围关键帧做好准备。// extract all the nearby key poses and downsample themkdtreeSurroundingKeyPoses->setInputCloud(cloudKeyPoses3D);
//用之前的机器人位置来建树kdtreeSurroundingKeyPoses->radiusSearch(currentRobotPosPoint, (double)surroundingKeyframeSearchRadius, pointSearchInd, pointSearchSqDis, 0);
//extern const float surroundingKeyframeSearchRadius = 50.0; 距离是50
// key frame that is within n meters from current pose will be considerd for scan-to-map optimization (when loop closure disabled)for (int i = 0; i < pointSearchInd.size(); ++i)surroundingKeyPoses->points.push_back(cloudKeyPoses3D->points[pointSearchInd[i]]);
//将找到的点添加到 surroundingKeyPoses 中。对提取到的 surroundingKeyPoses 进行降采样,得到 surroundingKeyPosesDS。降采样后可以减少计算量。downSizeFilterSurroundingKeyPoses.setInputCloud(surroundingKeyPoses);downSizeFilterSurroundingKeyPoses.filter(*surroundingKeyPosesDS);// delete key frames that are not in surrounding regionint numSurroundingPosesDS = surroundingKeyPosesDS->points.size();for (int i = 0; i < surroundingExistingKeyPosesID.size(); ++i){bool existingFlag = false;for (int j = 0; j < numSurroundingPosesDS; ++j){if (surroundingExistingKeyPosesID[i] == (int)surroundingKeyPosesDS->points[j].intensity){existingFlag = true;break;}}if (existingFlag == false){surroundingExistingKeyPosesID.   erase(surroundingExistingKeyPosesID.   begin() + i);surroundingCornerCloudKeyFrames. erase(surroundingCornerCloudKeyFrames. begin() + i);surroundingSurfCloudKeyFrames.   erase(surroundingSurfCloudKeyFrames.   begin() + i);surroundingOutlierCloudKeyFrames.erase(surroundingOutlierCloudKeyFrames.begin() + i);--i;}}// add new key frames that are not in calculated existing key framesfor (int i = 0; i < numSurroundingPosesDS; ++i) {bool existingFlag = false;for (auto iter = surroundingExistingKeyPosesID.begin(); iter != surroundingExistingKeyPosesID.end(); ++iter){if ((*iter) == (int)surroundingKeyPosesDS->points[i].intensity){existingFlag = true;break;}}if (existingFlag == true){continue;}else{int thisKeyInd = (int)surroundingKeyPosesDS->points[i].intensity;PointTypePose thisTransformation = cloudKeyPoses6D->points[thisKeyInd];updateTransformPointCloudSinCos(&thisTransformation);surroundingExistingKeyPosesID.   push_back(thisKeyInd);surroundingCornerCloudKeyFrames. push_back(transformPointCloud(cornerCloudKeyFrames[thisKeyInd]));surroundingSurfCloudKeyFrames.   push_back(transformPointCloud(surfCloudKeyFrames[thisKeyInd]));surroundingOutlierCloudKeyFrames.push_back(transformPointCloud(outlierCloudKeyFrames[thisKeyInd]));}}for (int i = 0; i < surroundingExistingKeyPosesID.size(); ++i) {*laserCloudCornerFromMap += *surroundingCornerCloudKeyFrames[i];*laserCloudSurfFromMap   += *surroundingSurfCloudKeyFrames[i];*laserCloudSurfFromMap   += *surroundingOutlierCloudKeyFrames[i];}}

 这里增加和删减关键帧的代码很有意思,我们来看一下:

先看增加的:

// add new key frames that are not in calculated existing key framesfor (int i = 0; i < numSurroundingPosesDS; ++i) {bool existingFlag = false;//首先将existingFlag都初始化为false。for (auto iter = surroundingExistingKeyPosesID.begin(); iter != surroundingExistingKeyPosesID.end(); ++iter){if ((*iter) == (int)surroundingKeyPosesDS->points[i].intensity){existingFlag = true;break;}}
//这一段代码是为了防止重复添加,如果关键帧已经在surroundingExistingKeyPosesID中,那么就break。if (existingFlag == true){continue;
//如果这一帧已经存在了,再去循环下一帧。}else{
//如果都不存在,那就添加进来,比如一开始的时候,surroundingExistingKeyPosesID没有值,肯定要添加!!int thisKeyInd = (int)surroundingKeyPosesDS->points[i].intensity;PointTypePose thisTransformation = cloudKeyPoses6D->points[thisKeyInd];updateTransformPointCloudSinCos(&thisTransformation);surroundingExistingKeyPosesID.   push_back(thisKeyInd);surroundingCornerCloudKeyFrames. push_back(transformPointCloud(cornerCloudKeyFrames[thisKeyInd]));surroundingSurfCloudKeyFrames.   push_back(transformPointCloud(surfCloudKeyFrames[thisKeyInd]));surroundingOutlierCloudKeyFrames.push_back(transformPointCloud(outlierCloudKeyFrames[thisKeyInd]));}}
for (int i = 0; i < surroundingExistingKeyPosesID.size(); ++i) {*laserCloudCornerFromMap += *surroundingCornerCloudKeyFrames[i];*laserCloudSurfFromMap   += *surroundingSurfCloudKeyFrames[i];*laserCloudSurfFromMap   += *surroundingOutlierCloudKeyFrames[i];
//离群点也加到面点中去了。}}

 再看删除的:

int numSurroundingPosesDS = surroundingKeyPosesDS->points.size();for (int i = 0; i < surroundingExistingKeyPosesID.size(); ++i){bool existingFlag = false;for (int j = 0; j < numSurroundingPosesDS; ++j){if (surroundingExistingKeyPosesID[i] == (int)surroundingKeyPosesDS->points[j].intensity){existingFlag = true;break;}}
//从新找到的周围帧中看看有没有老同志,如果有说明同志不老,如果没有说明太落后,可以踢掉了。if (existingFlag == false){surroundingExistingKeyPosesID.   erase(surroundingExistingKeyPosesID.   begin() + i);surroundingCornerCloudKeyFrames. erase(surroundingCornerCloudKeyFrames. begin() + i);surroundingSurfCloudKeyFrames.   erase(surroundingSurfCloudKeyFrames.   begin() + i);surroundingOutlierCloudKeyFrames.erase(surroundingOutlierCloudKeyFrames.begin() + i);--i;}}

 最后需要回环的和不需要回环的,得到的结果统一进行降采样,得到

laserCloudCornerFromMapDS和laserCloudSurfFromMapDS

// Downsample the surrounding corner key frames (or map)
downSizeFilterCorner.setInputCloud(laserCloudCornerFromMap);
downSizeFilterCorner.filter(*laserCloudCornerFromMapDS);
laserCloudCornerFromMapDSNum = laserCloudCornerFromMapDS->points.size();
// Downsample the surrounding surf key frames (or map)
downSizeFilterSurf.setInputCloud(laserCloudSurfFromMap);
downSizeFilterSurf.filter(*laserCloudSurfFromMapDS);
laserCloudSurfFromMapDSNum = laserCloudSurfFromMapDS->points.size();

downsampleCurrentScan();

void downsampleCurrentScan(){//downSizeFilterCorner.setLeafSize(0.2, 0.2, 0.2);
//downSizeFilterSurf.setLeafSize(0.4, 0.4, 0.4);
//downSizeFilterOutlier.setLeafSize(0.4, 0.4, 0.4);laserCloudCornerLastDS->clear();downSizeFilterCorner.setInputCloud(laserCloudCornerLast);downSizeFilterCorner.filter(*laserCloudCornerLastDS);laserCloudCornerLastDSNum = laserCloudCornerLastDS->points.size();laserCloudSurfLastDS->clear();downSizeFilterSurf.setInputCloud(laserCloudSurfLast);downSizeFilterSurf.filter(*laserCloudSurfLastDS);laserCloudSurfLastDSNum = laserCloudSurfLastDS->points.size();laserCloudOutlierLastDS->clear();downSizeFilterOutlier.setInputCloud(laserCloudOutlierLast);downSizeFilterOutlier.filter(*laserCloudOutlierLastDS);laserCloudOutlierLastDSNum = laserCloudOutlierLastDS->points.size();laserCloudSurfTotalLast->clear();laserCloudSurfTotalLastDS->clear();*laserCloudSurfTotalLast += *laserCloudSurfLastDS;*laserCloudSurfTotalLast += *laserCloudOutlierLastDS;downSizeFilterSurf.setInputCloud(laserCloudSurfTotalLast);downSizeFilterSurf.filter(*laserCloudSurfTotalLastDS);laserCloudSurfTotalLastDSNum = laserCloudSurfTotalLastDS->points.size();}

角点和面点按照不同大小的格子采样,格子越小采样点越多,越细致,所以角点要更密一些。

scan2MapOptimization();

 void scan2MapOptimization(){if (laserCloudCornerFromMapDSNum > 10 && laserCloudSurfFromMapDSNum > 100) {kdtreeCornerFromMap->setInputCloud(laserCloudCornerFromMapDS);//为角点建立kd树kdtreeSurfFromMap->setInputCloud(laserCloudSurfFromMapDS);//为面点建立kd树for (int iterCount = 0; iterCount < 10; iterCount++) {laserCloudOri->clear();coeffSel->clear();cornerOptimization(iterCount);surfOptimization(iterCount);if (LMOptimization(iterCount) == true)break;              }transformUpdate();}}

 似曾相识的操作,和FeatureAssociation中的updateTransformation()函数很像。一开始laserCloudCornerFromMapDSNum == 0 ,因为laserCloudCornerFromMapDSNum的值是在extractSurroundingKeyFrames();中更新的!!所以我们先看下一个函数saveKeyFramesAndFactor();。

经过extractSurroundingKeyFrames();,现在laserCloudCornerFromMapDSNum !=0,我们接着看scan2MapOptimization();。

cornerOptimization(iterCount);

updatePointAssociateToMapSinCos();
void updatePointAssociateToMapSinCos(){cRoll = cos(transformTobeMapped[0]);sRoll = sin(transformTobeMapped[0]);cPitch = cos(transformTobeMapped[1]);sPitch = sin(transformTobeMapped[1]);cYaw = cos(transformTobeMapped[2]);sYaw = sin(transformTobeMapped[2]);tX = transformTobeMapped[3];tY = transformTobeMapped[4];tZ = transformTobeMapped[5];}
 pointAssociateToMap(&pointOri, &pointSel);
 void pointAssociateToMap(PointType const * const pi, PointType * const po){float x1 = cYaw * pi->x - sYaw * pi->y;float y1 = sYaw * pi->x + cYaw * pi->y;float z1 = pi->z;float x2 = x1;float y2 = cRoll * y1 - sRoll * z1;float z2 = sRoll * y1 + cRoll * z1;po->x = cPitch * x2 + sPitch * z2 + tX;po->y = y2 + tY;po->z = -sPitch * x2 + cPitch * z2 + tZ;po->intensity = pi->intensity;}//将点云转换到当前优化之后的位姿!!
 void cornerOptimization(int iterCount){updatePointAssociateToMapSinCos();for (int i = 0; i < laserCloudCornerLastDSNum; i++) {pointOri = laserCloudCornerLastDS->points[i];//一定要区分开laserCloudCornerLastDS和laserCloudCornerFromMapDSNum!!
//laserCloudCornerLastDS 和 laserCloudCornerFromMapDSNum 是分别处理“当前帧”和“地图”中角点云数据的两个变量
//laserCloudCornerLastDS 表示当前激光帧中降采样后的角点云。//这是在当前帧的点云中选取的角点特征,通过降采样 (DownSample) 后存入 laserCloudCornerLastDS,以减少计算量和数据冗余。
//该点云用于与地图数据匹配的优化过程。//所以是用地图中的角点建树,用当前帧的角度查询!!!pointAssociateToMap(&pointOri, &pointSel);kdtreeCornerFromMap->nearestKSearch(pointSel, 5, pointSearchInd, pointSearchSqDis);//找到当前点相邻的五个点if (pointSearchSqDis[4] < 1.0) {//如果距离最大的那个点的距离都小于1,那么证明五个相邻点均满足要求。float cx = 0, cy = 0, cz = 0;for (int j = 0; j < 5; j++) {cx += laserCloudCornerFromMapDS->points[pointSearchInd[j]].x;cy += laserCloudCornerFromMapDS->points[pointSearchInd[j]].y;cz += laserCloudCornerFromMapDS->points[pointSearchInd[j]].z;}cx /= 5; cy /= 5;  cz /= 5;//cx,cy,cz是中心坐标。float a11 = 0, a12 = 0, a13 = 0, a22 = 0, a23 = 0, a33 = 0;for (int j = 0; j < 5; j++) {float ax = laserCloudCornerFromMapDS->points[pointSearchInd[j]].x - cx;float ay = laserCloudCornerFromMapDS->points[pointSearchInd[j]].y - cy;float az = laserCloudCornerFromMapDS->points[pointSearchInd[j]].z - cz;a11 += ax * ax; a12 += ax * ay; a13 += ax * az;a22 += ay * ay; a23 += ay * az;a33 += az * az;}a11 /= 5; a12 /= 5; a13 /= 5; a22 /= 5; a23 /= 5; a33 /= 5;//方差协方差的平均值//matA1是五个点到中心点距离的协方差矩阵的均值矩阵,也就是五个协方差矩阵的平均。matA1.at<float>(0, 0) = a11; matA1.at<float>(0, 1) = a12; matA1.at<float>(0, 2) = a13;matA1.at<float>(1, 0) = a12; matA1.at<float>(1, 1) = a22; matA1.at<float>(1, 2) = a23;matA1.at<float>(2, 0) = a13; matA1.at<float>(2, 1) = a23; matA1.at<float>(2, 2) = a33;cv::eigen(matA1, matD1, matV1);
//cv::eigen(matA1, matD1, matV1); 是 OpenCV 提供的一个函数,用于计算矩阵的特征值和特征向量。
//具体来说:
//    matA1 是输入矩阵,通常是一个方阵。
//    matD1 是输出矩阵,用于存储 matA1 的特征值。
//    matV1 是输出矩阵,用于存储 matA1 的特征向量。
//在执行后,matD1 中包含的是 matA1 的特征值,而 matV1 的列向量则是对应的特征向量。if (matD1.at<float>(0, 0) > 3 * matD1.at<float>(0, 1)) {float x0 = pointSel.x;float y0 = pointSel.y;float z0 = pointSel.z;float x1 = cx + 0.1 * matV1.at<float>(0, 0);float y1 = cy + 0.1 * matV1.at<float>(0, 1);float z1 = cz + 0.1 * matV1.at<float>(0, 2);float x2 = cx - 0.1 * matV1.at<float>(0, 0);float y2 = cy - 0.1 * matV1.at<float>(0, 1);float z2 = cz - 0.1 * matV1.at<float>(0, 2);
//这段代码的作用是基于特征向量 matV1 在三维空间中计算两个点 (x1, y1, z1) 和 (x2, y2, z2) 的位置。
//这两个点位于以 (cx, cy, cz) 为中心、沿特征向量方向的一小段距离(0.1 倍特征向量长度)上。float a012 = sqrt(((x0 - x1)*(y0 - y2) - (x0 - x2)*(y0 - y1))* ((x0 - x1)*(y0 - y2) - (x0 - x2)*(y0 - y1)) + ((x0 - x1)*(z0 - z2) - (x0 - x2)*(z0 - z1))* ((x0 - x1)*(z0 - z2) - (x0 - x2)*(z0 - z1)) + ((y0 - y1)*(z0 - z2) - (y0 - y2)*(z0 - z1))* ((y0 - y1)*(z0 - z2) - (y0 - y2)*(z0 - z1)));float l12 = sqrt((x1 - x2)*(x1 - x2) + (y1 - y2)*(y1 - y2) + (z1 - z2)*(z1 - z2));float la = ((y1 - y2)*((x0 - x1)*(y0 - y2) - (x0 - x2)*(y0 - y1)) + (z1 - z2)*((x0 - x1)*(z0 - z2) - (x0 - x2)*(z0 - z1))) / a012 / l12;float lb = -((x1 - x2)*((x0 - x1)*(y0 - y2) - (x0 - x2)*(y0 - y1)) - (z1 - z2)*((y0 - y1)*(z0 - z2) - (y0 - y2)*(z0 - z1))) / a012 / l12;float lc = -((x1 - x2)*((x0 - x1)*(z0 - z2) - (x0 - x2)*(z0 - z1)) + (y1 - y2)*((y0 - y1)*(z0 - z2) - (y0 - y2)*(z0 - z1))) / a012 / l12;float ld2 = a012 / l12;float s = 1 - 0.9 * fabs(ld2);
//以上代码在FeatureAssociation中的updateTransformation()函数注释过,可以去之前的博客查看,
//具体代码作用就是找到当前帧角点到“地图”上的线特征的垂线方向和距离。coeff.x = s * la;coeff.y = s * lb;coeff.z = s * lc;coeff.intensity = s * ld2;if (s > 0.1) {laserCloudOri->push_back(pointOri);coeffSel->push_back(coeff);}}}}}

if (matD1.at<float>(0, 0) > 3 * matD1.at<float>(0, 1)),matD1.at<float>(0, 0) 表示第一个特征值,matD1.at<float>(0, 1) 表示第二个特征值,确认数据集中一个方向的变化比其他方向大得多,这里是判断5个相邻点是不是在一条直线上

 surfOptimization(iterCount);

void surfOptimization(int iterCount){updatePointAssociateToMapSinCos();for (int i = 0; i < laserCloudSurfTotalLastDSNum; i++) {pointOri = laserCloudSurfTotalLastDS->points[i];pointAssociateToMap(&pointOri, &pointSel); kdtreeSurfFromMap->nearestKSearch(pointSel, 5, pointSearchInd, pointSearchSqDis);if (pointSearchSqDis[4] < 1.0) {for (int j = 0; j < 5; j++) {matA0.at<float>(j, 0) = laserCloudSurfFromMapDS->points[pointSearchInd[j]].x;matA0.at<float>(j, 1) = laserCloudSurfFromMapDS->points[pointSearchInd[j]].y;matA0.at<float>(j, 2) = laserCloudSurfFromMapDS->points[pointSearchInd[j]].z;}cv::solve(matA0, matB0, matX0, cv::DECOMP_QR);float pa = matX0.at<float>(0, 0);float pb = matX0.at<float>(1, 0);float pc = matX0.at<float>(2, 0);float pd = 1;float ps = sqrt(pa * pa + pb * pb + pc * pc);pa /= ps; pb /= ps; pc /= ps; pd /= ps;bool planeValid = true;for (int j = 0; j < 5; j++) {if (fabs(pa * laserCloudSurfFromMapDS->points[pointSearchInd[j]].x +pb * laserCloudSurfFromMapDS->points[pointSearchInd[j]].y +pc * laserCloudSurfFromMapDS->points[pointSearchInd[j]].z + pd) > 0.2) {planeValid = false;break;}}if (planeValid) {float pd2 = pa * pointSel.x + pb * pointSel.y + pc * pointSel.z + pd;float s = 1 - 0.9 * fabs(pd2) / sqrt(sqrt(pointSel.x * pointSel.x+ pointSel.y * pointSel.y + pointSel.z * pointSel.z));coeff.x = s * pa;coeff.y = s * pb;coeff.z = s * pc;coeff.intensity = s * pd2;if (s > 0.1) {laserCloudOri->push_back(pointOri);coeffSel->push_back(coeff);}}}}}

 这段用特征值判断是面特征的代码很有意思,拿出来看一下:

 cv::solve(matA0, matB0, matX0, cv::DECOMP_QR);float pa = matX0.at<float>(0, 0);float pb = matX0.at<float>(1, 0);float pc = matX0.at<float>(2, 0);float pd = 1;float ps = sqrt(pa * pa + pb * pb + pc * pc);pa /= ps; pb /= ps; pc /= ps; pd /= ps;//将pa,pb,pc,pd归一化。bool planeValid = true;for (int j = 0; j < 5; j++) {if (fabs(pa * laserCloudSurfFromMapDS->points[pointSearchInd[j]].x +pb * laserCloudSurfFromMapDS->points[pointSearchInd[j]].y +pc * laserCloudSurfFromMapDS->points[pointSearchInd[j]].z + pd) > 0.2) {planeValid = false;break;}}

 cv::solve(matA0, matB0, matX0, cv::DECOMP_QR);

matA0 = cv::Mat (5, 3, CV_32F, cv::Scalar::all(0));
matB0 = cv::Mat (5, 1, CV_32F, cv::Scalar::all(-1));
matX0 = cv::Mat (3, 1, CV_32F, cv::Scalar::all(0));

使用 cv::solve 函数解线性方程组,得到平面方程的法向量(平面参数)结果 matX0。该平面方程满足 ax + by + cz + d = 0

  • 对拟合平面的 5 个点逐个进行检查。
  • 距离计算:对于每个点,计算它到平面的距离,即代入平面方程 ax + by + cz + d = 0 后的结果。这个值的绝对值表示该点到平面的“偏离程度”。
  • 判断条件:如果任意一个点的偏离值大于 0.2(即距离平面超过 0.2 米),则认为该平面不可靠,将 planeValid 设为 false 并跳出循环。

 LMOptimization(iterCount);

bool LMOptimization(int iterCount){float srx = sin(transformTobeMapped[0]);float crx = cos(transformTobeMapped[0]);float sry = sin(transformTobeMapped[1]);float cry = cos(transformTobeMapped[1]);float srz = sin(transformTobeMapped[2]);float crz = cos(transformTobeMapped[2]);int laserCloudSelNum = laserCloudOri->points.size();if (laserCloudSelNum < 50) {return false;//如果参与优化的点太少,给老子回炉重造!!}cv::Mat matA(laserCloudSelNum, 6, CV_32F, cv::Scalar::all(0));cv::Mat matAt(6, laserCloudSelNum, CV_32F, cv::Scalar::all(0));cv::Mat matAtA(6, 6, CV_32F, cv::Scalar::all(0));cv::Mat matB(laserCloudSelNum, 1, CV_32F, cv::Scalar::all(0));cv::Mat matAtB(6, 1, CV_32F, cv::Scalar::all(0));cv::Mat matX(6, 1, CV_32F, cv::Scalar::all(0));for (int i = 0; i < laserCloudSelNum; i++) {pointOri = laserCloudOri->points[i];coeff = coeffSel->points[i];float arx = (crx*sry*srz*pointOri.x + crx*crz*sry*pointOri.y - srx*sry*pointOri.z) * coeff.x+ (-srx*srz*pointOri.x - crz*srx*pointOri.y - crx*pointOri.z) * coeff.y+ (crx*cry*srz*pointOri.x + crx*cry*crz*pointOri.y - cry*srx*pointOri.z) * coeff.z;float ary = ((cry*srx*srz - crz*sry)*pointOri.x + (sry*srz + cry*crz*srx)*pointOri.y + crx*cry*pointOri.z) * coeff.x+ ((-cry*crz - srx*sry*srz)*pointOri.x + (cry*srz - crz*srx*sry)*pointOri.y - crx*sry*pointOri.z) * coeff.z;float arz = ((crz*srx*sry - cry*srz)*pointOri.x + (-cry*crz-srx*sry*srz)*pointOri.y)*coeff.x+ (crx*crz*pointOri.x - crx*srz*pointOri.y) * coeff.y+ ((sry*srz + cry*crz*srx)*pointOri.x + (crz*sry-cry*srx*srz)*pointOri.y)*coeff.z;matA.at<float>(i, 0) = arx;matA.at<float>(i, 1) = ary;matA.at<float>(i, 2) = arz;matA.at<float>(i, 3) = coeff.x;matA.at<float>(i, 4) = coeff.y;matA.at<float>(i, 5) = coeff.z;matB.at<float>(i, 0) = -coeff.intensity;}cv::transpose(matA, matAt);matAtA = matAt * matA;matAtB = matAt * matB;cv::solve(matAtA, matAtB, matX, cv::DECOMP_QR);if (iterCount == 0) {cv::Mat matE(1, 6, CV_32F, cv::Scalar::all(0));cv::Mat matV(6, 6, CV_32F, cv::Scalar::all(0));cv::Mat matV2(6, 6, CV_32F, cv::Scalar::all(0));cv::eigen(matAtA, matE, matV);matV.copyTo(matV2);isDegenerate = false;float eignThre[6] = {100, 100, 100, 100, 100, 100};for (int i = 5; i >= 0; i--) {if (matE.at<float>(0, i) < eignThre[i]) {for (int j = 0; j < 6; j++) {matV2.at<float>(i, j) = 0;}isDegenerate = true;} else {break;}}matP = matV.inv() * matV2;}if (isDegenerate) {cv::Mat matX2(6, 1, CV_32F, cv::Scalar::all(0));matX.copyTo(matX2);matX = matP * matX2;}transformTobeMapped[0] += matX.at<float>(0, 0);transformTobeMapped[1] += matX.at<float>(1, 0);transformTobeMapped[2] += matX.at<float>(2, 0);transformTobeMapped[3] += matX.at<float>(3, 0);transformTobeMapped[4] += matX.at<float>(4, 0);transformTobeMapped[5] += matX.at<float>(5, 0);float deltaR = sqrt(pow(pcl::rad2deg(matX.at<float>(0, 0)), 2) +pow(pcl::rad2deg(matX.at<float>(1, 0)), 2) +pow(pcl::rad2deg(matX.at<float>(2, 0)), 2));float deltaT = sqrt(pow(matX.at<float>(3, 0) * 100, 2) +pow(matX.at<float>(4, 0) * 100, 2) +pow(matX.at<float>(5, 0) * 100, 2));if (deltaR < 0.05 && deltaT < 0.05) {return true;}return false;}

 这段代码实现了一个基于 LM(Levenberg-Marquardt)优化的位姿调整函数 LMOptimization,其中每次迭代会根据点云的配准误差,调整并优化位姿参数 transformTobeMapped

  • matA 存储每个点的雅可比矩阵,用于表达平移和旋转对误差的影响。
  • matB 存储误差值,表示点到平面/线的距离误差。
配准的优化目标是最小化点 pointOri 到目标平面的距离,因此误差函数可以定义为:
error=n⃗⋅(R(transformTobeMapped)⋅p⃗Ori+t⃗)−d
其中:
n⃗为平面法向量,即 coeff 的 x, y, z 分量。
p⃗Ori为点 pointOri。
R 和 t 为旋转和平移量。
d 是平面到原点的距离。
优化时,误差对各位姿参数的偏导数就构成了雅可比矩阵的各个元素。
  • 求解线性方程 matAtA * matX = matAtB 得到 matX,即位姿更新值。

公式的具体推导如下:

1. 建立误差方程在位姿优化中,对于每个观测点,定义误差 eiei​:
ei=f(pi,X)−qi
ei​=f(pi​,X)−qi​其中:pipi​ 是观测的点。XX 是待优化的位姿(旋转和平移)参数。f(pi,X)f(pi​,X) 是在位姿参数 XX 下点 pipi​ 在地图中的预测位置。qiqi​ 是点 pipi​ 应该满足的目标位置(或平面约束)。2. 最小化平方误差为了找到最优的 XX,最小化所有观测点的误差平方和,即目标函数:
E(X)=∑iei2
E(X)=i∑​ei2​将上式展开,变成优化问题。
3. 线性化误差方程LM 优化是一种非线性优化方法,它首先将误差方程线性化,以便能够用线性方程来近似求解更新量。假设当前位姿估计为 XX,那么误差可以用泰勒展开近似表示:
ei(X+ΔX)≈ei(X)+∂ei∂XΔX
ei​(X+ΔX)≈ei​(X)+∂X∂ei​​ΔX定义雅可比矩阵 Ji=∂ei∂XJi​=∂X∂ei​​,可以简化为:
e≈JΔX+b
e≈JΔX+b其中 bb 是当前误差值。
4. 通过正规方程求解最优解将上面的误差向量放入最小化问题,并对 ΔXΔX 求导,得到正规方程:
JTJΔX=JTb
JTJΔX=JTb其中:JTJJTJ 对应 matAtA。JTbJTb 对应 matAtB。将这个方程用 QR 分解或其他解线性方程的方式求出 ΔXΔX,即为位姿更新量

 在第一次迭代时,要判断matAtA的奇异性,处理退化情况。

if (iterCount == 0) {cv::Mat matE(1, 6, CV_32F, cv::Scalar::all(0));cv::Mat matV(6, 6, CV_32F, cv::Scalar::all(0));cv::Mat matV2(6, 6, CV_32F, cv::Scalar::all(0));cv::eigen(matAtA, matE, matV);matV.copyTo(matV2);isDegenerate = false;float eignThre[6] = {100, 100, 100, 100, 100, 100};for (int i = 5; i >= 0; i--) {if (matE.at<float>(0, i) < eignThre[i]) {//将特征值小于100,对应的特征向量置零。for (int j = 0; j < 6; j++) {matV2.at<float>(i, j) = 0;}isDegenerate = true;} else {break;}}matP = matV.inv() * matV2;//matV.inv() 是 matV 的逆矩阵,matP对应的就是除了退化的列为0,其他的列均为1。}

 transformUpdate();

void transformUpdate(){if (imuPointerLast >= 0) {float imuRollLast = 0, imuPitchLast = 0;while (imuPointerFront != imuPointerLast) {if (timeLaserOdometry + scanPeriod < imuTime[imuPointerFront]) {break;}imuPointerFront = (imuPointerFront + 1) % imuQueLength;}if (timeLaserOdometry + scanPeriod > imuTime[imuPointerFront]) {imuRollLast = imuRoll[imuPointerFront];imuPitchLast = imuPitch[imuPointerFront];} else {int imuPointerBack = (imuPointerFront + imuQueLength - 1) % imuQueLength;float ratioFront = (timeLaserOdometry + scanPeriod - imuTime[imuPointerBack]) / (imuTime[imuPointerFront] - imuTime[imuPointerBack]);float ratioBack = (imuTime[imuPointerFront] - timeLaserOdometry - scanPeriod) / (imuTime[imuPointerFront] - imuTime[imuPointerBack]);imuRollLast = imuRoll[imuPointerFront] * ratioFront + imuRoll[imuPointerBack] * ratioBack;imuPitchLast = imuPitch[imuPointerFront] * ratioFront + imuPitch[imuPointerBack] * ratioBack;}transformTobeMapped[0] = 0.998 * transformTobeMapped[0] + 0.002 * imuPitchLast;transformTobeMapped[2] = 0.998 * transformTobeMapped[2] + 0.002 * imuRollLast;}for (int i = 0; i < 6; i++) {transformBefMapped[i] = transformSum[i];transformAftMapped[i] = transformTobeMapped[i];}}

 saveKeyFramesAndFactor();

void saveKeyFramesAndFactor(){currentRobotPosPoint.x = transformAftMapped[3];currentRobotPosPoint.y = transformAftMapped[4];currentRobotPosPoint.z = transformAftMapped[5];
//第一次运行时,transformAftMapped初始化为0,xyz均为0。bool saveThisKeyFrame = true;if (sqrt((previousRobotPosPoint.x-currentRobotPosPoint.x)*(previousRobotPosPoint.x-currentRobotPosPoint.x)+(previousRobotPosPoint.y-currentRobotPosPoint.y)*(previousRobotPosPoint.y-currentRobotPosPoint.y)+(previousRobotPosPoint.z-currentRobotPosPoint.z)*(previousRobotPosPoint.z-currentRobotPosPoint.z)) < 0.3){saveThisKeyFrame = false;
//如果两帧之间的距离太小,就不保存!!}
//所以第一次运行时,saveThisKeyFrame的值肯定是false!!if (saveThisKeyFrame == false && !cloudKeyPoses3D->points.empty())return;
//所以关键点在这,第一次运行时,也不会return,因为cloudKeyPoses3D->points.empty()。previousRobotPosPoint = currentRobotPosPoint;/*** update grsam graph*/if (cloudKeyPoses3D->points.empty()){gtSAMgraph.add(PriorFactor<Pose3>(0, Pose3(Rot3::RzRyRx(transformTobeMapped[2], transformTobeMapped[0], transformTobeMapped[1]),Point3(transformTobeMapped[5], transformTobeMapped[3], transformTobeMapped[4])), priorNoise));initialEstimate.insert(0, Pose3(Rot3::RzRyRx(transformTobeMapped[2], transformTobeMapped[0], transformTobeMapped[1]),Point3(transformTobeMapped[5], transformTobeMapped[3], transformTobeMapped[4])));for (int i = 0; i < 6; ++i)transformLast[i] = transformTobeMapped[i];}else{gtsam::Pose3 poseFrom = Pose3(Rot3::RzRyRx(transformLast[2], transformLast[0], transformLast[1]),Point3(transformLast[5], transformLast[3], transformLast[4]));gtsam::Pose3 poseTo   = Pose3(Rot3::RzRyRx(transformAftMapped[2], transformAftMapped[0], transformAftMapped[1]),Point3(transformAftMapped[5], transformAftMapped[3], transformAftMapped[4]));gtSAMgraph.add(BetweenFactor<Pose3>(cloudKeyPoses3D->points.size()-1, cloudKeyPoses3D->points.size(), poseFrom.between(poseTo), odometryNoise));initialEstimate.insert(cloudKeyPoses3D->points.size(), Pose3(Rot3::RzRyRx(transformAftMapped[2], transformAftMapped[0], transformAftMapped[1]),Point3(transformAftMapped[5], transformAftMapped[3], transformAftMapped[4])));}/*** update iSAM*/isam->update(gtSAMgraph, initialEstimate);isam->update();gtSAMgraph.resize(0);initialEstimate.clear();/*** save key poses*/PointType thisPose3D;PointTypePose thisPose6D;Pose3 latestEstimate;isamCurrentEstimate = isam->calculateEstimate();latestEstimate = isamCurrentEstimate.at<Pose3>(isamCurrentEstimate.size()-1);thisPose3D.x = latestEstimate.translation().y();thisPose3D.y = latestEstimate.translation().z();thisPose3D.z = latestEstimate.translation().x();thisPose3D.intensity = cloudKeyPoses3D->points.size(); // this can be used as indexcloudKeyPoses3D->push_back(thisPose3D);thisPose6D.x = thisPose3D.x;thisPose6D.y = thisPose3D.y;thisPose6D.z = thisPose3D.z;thisPose6D.intensity = thisPose3D.intensity; // this can be used as indexthisPose6D.roll  = latestEstimate.rotation().pitch();thisPose6D.pitch = latestEstimate.rotation().yaw();thisPose6D.yaw   = latestEstimate.rotation().roll(); // in camera framethisPose6D.time = timeLaserOdometry;cloudKeyPoses6D->push_back(thisPose6D);/*** save updated transform*/if (cloudKeyPoses3D->points.size() > 1){transformAftMapped[0] = latestEstimate.rotation().pitch();transformAftMapped[1] = latestEstimate.rotation().yaw();transformAftMapped[2] = latestEstimate.rotation().roll();transformAftMapped[3] = latestEstimate.translation().y();transformAftMapped[4] = latestEstimate.translation().z();transformAftMapped[5] = latestEstimate.translation().x();for (int i = 0; i < 6; ++i){transformLast[i] = transformAftMapped[i];transformTobeMapped[i] = transformAftMapped[i];}}pcl::PointCloud<PointType>::Ptr thisCornerKeyFrame(new pcl::PointCloud<PointType>());pcl::PointCloud<PointType>::Ptr thisSurfKeyFrame(new pcl::PointCloud<PointType>());pcl::PointCloud<PointType>::Ptr thisOutlierKeyFrame(new pcl::PointCloud<PointType>());pcl::copyPointCloud(*laserCloudCornerLastDS,  *thisCornerKeyFrame);pcl::copyPointCloud(*laserCloudSurfLastDS,    *thisSurfKeyFrame);pcl::copyPointCloud(*laserCloudOutlierLastDS, *thisOutlierKeyFrame);cornerCloudKeyFrames.push_back(thisCornerKeyFrame);surfCloudKeyFrames.push_back(thisSurfKeyFrame);outlierCloudKeyFrames.push_back(thisOutlierKeyFrame);    
}

 我们仔细看一下这段代码,这段代码考虑第一次优化时的情况:

 if (cloudKeyPoses3D->points.empty()){gtSAMgraph.add(PriorFactor<Pose3>(0, Pose3(Rot3::RzRyRx(transformTobeMapped[2], transformTobeMapped[0], transformTobeMapped[1]),Point3(transformTobeMapped[5], transformTobeMapped[3], transformTobeMapped[4])), priorNoise));
//这里将一个先验因子添加到因子图 gtSAMgraph 中,以指定第一个关键帧的位置和姿态。这是为了固定第一个关键帧,作为优化的参考基准。
//    PriorFactor<Pose3>(0, ...) 以第一个关键帧(索引 0)为对象,提供先验位姿。
//    Pose3(Rot3::RzRyRx(...), Point3(...)) 用于表示位姿,其中 Rot3::RzRyRx(...) 表示旋转角度,Point3(...) 表示平移。
//    priorNoise 是因子的噪声模型,用于定义先验约束的置信度。initialEstimate.insert(0, Pose3(Rot3::RzRyRx(transformTobeMapped[2], transformTobeMapped[0], transformTobeMapped[1]),Point3(transformTobeMapped[5], transformTobeMapped[3], transformTobeMapped[4])));
//将第一个关键帧位姿插入到 initialEstimate 中,作为优化的初始估计值。for (int i = 0; i < 6; ++i)transformLast[i] = transformTobeMapped[i];}

当我们的因子图中有第一个因子之后,就可以添加到cloudKeyPoses3D和cloudKeyPoses6D中。

注意这里又做了一次坐标轴的变换,x->y,y->z,z->x

cloudKeyPoses3D代表点,也就是位置,position。

thisPose3D.x = latestEstimate.translation().y();
thisPose3D.y = latestEstimate.translation().z();
thisPose3D.z = latestEstimate.translation().x();
thisPose3D.intensity = cloudKeyPoses3D->points.size(); // this can be used as index
//这里的强度可以作为点的索引。
cloudKeyPoses3D->push_back(thisPose3D);

 cloudKeyPoses6D代表位姿,也就是变换,transform。

thisPose6D.x = thisPose3D.x;
thisPose6D.y = thisPose3D.y;
thisPose6D.z = thisPose3D.z;
thisPose6D.intensity = thisPose3D.intensity; // this can be used as index
thisPose6D.roll  = latestEstimate.rotation().pitch();
thisPose6D.pitch = latestEstimate.rotation().yaw();
thisPose6D.yaw   = latestEstimate.rotation().roll(); // in camera frame
thisPose6D.time = timeLaserOdometry;
cloudKeyPoses6D->push_back(thisPose6D);
pcl::PointCloud<PointType>::Ptr thisCornerKeyFrame(new pcl::PointCloud<PointType>());
pcl::PointCloud<PointType>::Ptr thisSurfKeyFrame(new pcl::PointCloud<PointType>());
pcl::PointCloud<PointType>::Ptr thisOutlierKeyFrame(new pcl::PointCloud<PointType>());
pcl::copyPointCloud(*laserCloudCornerLastDS,  *thisCornerKeyFrame);
pcl::copyPointCloud(*laserCloudSurfLastDS,    *thisSurfKeyFrame);
pcl::copyPointCloud(*laserCloudOutlierLastDS, *thisOutlierKeyFrame);
//使用 pcl::copyPointCloud 将 laserCloudCornerLastDS、laserCloudSurfLastDS 和 
//laserCloudOutlierLastDS 点云数据复制到各自的关键帧中:
//    laserCloudCornerLastDS 是最新一帧经过降采样后的角点点云数据。
//    laserCloudSurfLastDS 是最新一帧经过降采样后的平面点数据。
//    laserCloudOutlierLastDS 是最新一帧经过降采样后的离群点数据。
cornerCloudKeyFrames.push_back(thisCornerKeyFrame);
surfCloudKeyFrames.push_back(thisSurfKeyFrame);
outlierCloudKeyFrames.push_back(thisOutlierKeyFrame);

这时咱们就可以回头看extractSurroundingKeyFrames();了。

 看完上述所有代码,咱们继续看saveKeyFramesAndFactor();

我们来看更新因子图的代码,之前我们看过图初始化的代码,现在我们来看初始化完成后的情况:

/**

* update grsam graph

*/

else{gtsam::Pose3 poseFrom = Pose3(Rot3::RzRyRx(transformLast[2], transformLast[0], transformLast[1]),Point3(transformLast[5], transformLast[3], transformLast[4]));gtsam::Pose3 poseTo   = Pose3(Rot3::RzRyRx(transformAftMapped[2], transformAftMapped[0], transformAftMapped[1]),Point3(transformAftMapped[5], transformAftMapped[3], transformAftMapped[4]));gtSAMgraph.add(BetweenFactor<Pose3>(cloudKeyPoses3D->points.size()-1, cloudKeyPoses3D->points.size(), poseFrom.between(poseTo), odometryNoise));initialEstimate.insert(cloudKeyPoses3D->points.size(), Pose3(Rot3::RzRyRx(transformAftMapped[2], transformAftMapped[0], transformAftMapped[1]),Point3(transformAftMapped[5], transformAftMapped[3], transformAftMapped[4])));}

transformLast相当于上上帧优化的位姿,transformAftMapped是上一帧优化的位姿,

添加因子

  • gtSAMgraph.add(BetweenFactor<Pose3>(...)):这一行代码将一个"位姿间因子"添加到因子图中。
  • BetweenFactor 用于表示两个位姿之间的约束(即 poseFromposeTo 之间的相对位姿)。这里使用 between 方法来计算两个位姿之间的相对变换,并将这个因子添加到图中,同时还包含了 odometryNoise 作为噪声模型。
odometryNoise = noiseModel::Diagonal::Variances(Vector6);

noiseModel::Diagonal::Variances

  • 这是GTSAM库中用于定义噪声模型的一个函数。噪声模型用于表示在测量过程中可能存在的不确定性,通常用于优化问题。
  • Diagonal表示这个噪声模型是对角线形式,意味着每个维度的噪声是独立的。
  1. 初始估计的插入

    • initialEstimate.insert(...):这行代码将 poseTo(表示后映射的位姿)插入到初始估计中。
    • 这里使用的旋转和平移量与 poseTo 中使用的相同,确保初始估计与因子图中的因子一致。

总的来说,这段代码的功能是从给定的转换数据(transformLasttransformAftMapped)中提取位姿信息,构建两个位姿之间的约束,并将这些约束添加到优化图中,同时为新位姿提供初始估计。

/*** update iSAM*/isam->update(gtSAMgraph, initialEstimate);isam->update();gtSAMgraph.resize(0);initialEstimate.clear();/*** save key poses*/PointType thisPose3D;PointTypePose thisPose6D;Pose3 latestEstimate;isamCurrentEstimate = isam->calculateEstimate();latestEstimate = isamCurrentEstimate.at<Pose3>(isamCurrentEstimate.size()-1);

 问:这种优化的逻辑是什么?为什么要这样优化?

优化的逻辑

  1. 因子图模型

    • 在因子图中,节点通常表示变量(如机器人或传感器的位姿),边(因子)表示观测或约束(如里程计数据、传感器测量等)。
    • 每个因子包含关于变量之间关系的信息,以及它们的噪声模型,提供了从一个状态(位姿)到另一个状态的测量或预测。
  2. 最小化误差

    • 优化过程的目标是最小化所有因子的误差函数。每个因子的误差反映了当前位姿估计与实际观测之间的差距。
    • 通过调整位姿估计,使得所有因子的误差尽可能小,从而得到一个全局一致的状态估计。(我觉得这一点很重要,全局一致
  3. 非线性最小二乘

    • GTSAM通常使用非线性最小二乘法(如高斯-牛顿法或莱文伯格-马夸尔特法)来求解这个优化问题。
    • 该方法迭代地调整变量以最小化残差,直到达到收敛条件。

correctPoses()

void correctPoses(){if (aLoopIsClosed == true){recentCornerCloudKeyFrames. clear();recentSurfCloudKeyFrames.   clear();recentOutlierCloudKeyFrames.clear();// update key posesint numPoses = isamCurrentEstimate.size();for (int i = 0; i < numPoses; ++i){cloudKeyPoses3D->points[i].x = isamCurrentEstimate.at<Pose3>(i).translation().y();cloudKeyPoses3D->points[i].y = isamCurrentEstimate.at<Pose3>(i).translation().z();cloudKeyPoses3D->points[i].z = isamCurrentEstimate.at<Pose3>(i).translation().x();cloudKeyPoses6D->points[i].x = cloudKeyPoses3D->points[i].x;cloudKeyPoses6D->points[i].y = cloudKeyPoses3D->points[i].y;cloudKeyPoses6D->points[i].z = cloudKeyPoses3D->points[i].z;cloudKeyPoses6D->points[i].roll  = isamCurrentEstimate.at<Pose3>(i).rotation().pitch();cloudKeyPoses6D->points[i].pitch = isamCurrentEstimate.at<Pose3>(i).rotation().yaw();cloudKeyPoses6D->points[i].yaw   = isamCurrentEstimate.at<Pose3>(i).rotation().roll();}aLoopIsClosed = false;}}

 aLoopIsClosed = false; aLoopIsClosed的初值是false,经过回环检测函数performLoopClosure()会变成true,我们先看回环检测。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/884702.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【大数据学习 | kafka】producer之拦截器,序列化器与分区器

1. 自定义拦截器 interceptor是拦截器&#xff0c;可以拦截到发送到kafka中的数据进行二次处理&#xff0c;它是producer组成部分的第一个组件。 public static class MyInterceptor implements ProducerInterceptor<String,String>{Overridepublic ProducerRecord<…

基于Spring Boot的高校物品捐赠管理系统设计与实现,LW+源码+讲解

摘 要 传统办法管理信息首先需要花费的时间比较多&#xff0c;其次数据出错率比较高&#xff0c;而且对错误的数据进行更改也比较困难&#xff0c;最后&#xff0c;检索数据费事费力。因此&#xff0c;在计算机上安装高校物品捐赠管理系统软件来发挥其高效地信息处理的作用&a…

推荐FileLink数据跨网摆渡系统 — 安全、高效的数据传输解决方案

在数字化转型的浪潮中&#xff0c;企业对于数据传输的需求日益增加&#xff0c;特别是在不同网络环境之间的文件共享和传输。为了满足这一需求&#xff0c;FileLink数据跨网摆渡系统应运而生&#xff0c;为企业提供了一种安全、高效的数据传输解决方案。 安全第一&#xff0c;保…

C++_day2

目录 1. 引用 reference&#xff08;重点&#xff09; 1.1 基础使用 1.2 特性 1.3 引用参数 2. C窄化&#xff08;了解&#xff09; 3. 输入&#xff08;熟悉&#xff09; 4. string 字符串类&#xff08;掌握&#xff09; 4.1 基础使用 4.2 取出元素 4.3 字符串与数字转换 5. …

服务器数据恢复—RAID5阵列硬盘坏道掉线导致存储不可用的数据恢复案例

服务器存储数据恢复环境&#xff1a; 一台EqualLogic存储中有一组由16块SAS硬盘组建的RAID5阵列。上层划分了4个卷&#xff0c;采用VMFS文件系统&#xff0c;存放虚拟机文件。 服务器存储故障&#xff1a; 存储RAID5阵列中磁盘出现故障&#xff0c;有2块硬盘对应的指示灯亮黄灯…

25国考照片处理器使用流程图解❗

1、打开“国家公务员局”网站&#xff0c;进入2025公务员专题&#xff0c;找到考生考务入口 2、点击下载地址 3、这几个下载链接都可以 4、下载压缩包 5、解压后先看“使用说明”&#xff0c;再找到“照片处理工具”双击。 6、双击后会进入这样的界面&#xff0c;点击&…

2024年云手机推荐榜单:高性能云手机推荐

无论是手游玩家、APP测试人员&#xff0c;还是数字营销工作者&#xff0c;云手机都为他们带来了极大的便利。本文将为大家推荐几款在市场上表现优异的云手机&#xff0c;希望这篇推荐指南可以帮助大家找到最适合自己的云手机&#xff01; 1. OgPhone云手机 OgPhone云手机是一款…

JeecgBoot入门

最近在了解低代码平台&#xff0c;其中关注到gitee上开源项目JeecgBoot&#xff0c;JeecgBoot官方也有比较完整的入门教学文档&#xff0c;这里我们将耕者官方教程学习&#xff0c;并将其记录下来。 一、项目简介 JeecgBoot 是一款基于代码生成器的低代码开发平台拥有零代码能力…

Java基础语法①Java特点+环境安装+IDEA使用

目录 1. Java的概念和用途 1.1 Java的概念和发展史 1.2 Java的重要性 1.3 Java的特点 2. Java环境 2.1 JVM 和 JDK 2.2 Java环境安装 2.3 安装IntelliJ IDEA并使用 2.4 IntelliJ IDEA常见快捷键 2.4.1 普通快捷键 2.4.2 调试快捷键 2.4.3 代码生成快捷键 本篇完 写…

windows与windows文件共享

目录 基础设置主机共享文件端设置从机接受文件端设置 基础设置 1、先确保两台电脑直接能够ping通&#xff0c;这是文件共享的前提&#xff0c;如果ping不通就去查找对应的原因&#xff0c;一般都是防火墙的原因。 在ping通的情况下&#xff1a; 2、先找到高级共享设置 3、对专…

Unity3D 开发教程:从入门到精通

Unity3D 开发教程&#xff1a;从入门到精通 Unity3D 是一款强大的跨平台游戏引擎&#xff0c;广泛应用于游戏开发、虚拟现实、增强现实等领域。本文将详细介绍 Unity3D 的基本概念、开发流程以及一些高级技巧&#xff0c;帮助你从零基础到掌握 Unity3D 开发。 目录 Unity3D…

[算法初阶]第二集 滑动窗口(已完结)

大家好啊,好久没有更新了,最近比较忙,所以来更新初阶算法,正好复习一下,感谢大家的观看,如有错误欢迎指出。 下面我们来看题目吧&#xff01; 1.209. 长度最小的子数组 这题大家想必一眼就看出了解法一暴力法 这个解法很简单 代码如下,不做多的解释 class Solution { publi…

恋爱脑学Rust之Box与RC的对比

在遥远的某个小镇&#xff0c;住着一对年轻的恋人&#xff1a;阿丽和小明。他们的爱情故事就像 Rust 中的 Rc 和 Box 智能指针那样&#xff0c;有着各自不同的「所有权」和「共享」的理解。 故事背景 阿丽和小明准备共同养一株非常珍贵的花&#xff08;我们称之为“心之花”&…

Move Dev Meetup@Beijing圆满结束,发掘Move生态新机会

Sui、Aptos 、Rooch 和 zkMove等为代表的 Move 生态在 2024 年展现出强劲的生命力和发展速度。随着技术的持续演进&#xff0c;Move 语言的独特优势吸引了大量优质项目、开发者、投资者和爱好者的参与&#xff0c;共同推动生态建设和创新应用。新一轮技术革新已经开启&#xff…

Python | Leetcode Python题解之第530题二叉搜索树的最小绝对差

题目&#xff1a; 题解&#xff1a; # Definition for a binary tree node. # class TreeNode(object): # def __init__(self, x): # self.val x # self.left None # self.right Noneclass Solution(object):def isValidBST(self, root):"…

数字经济赋能新质生产力数据集-dta格式(2012-2022年)

数据简介&#xff1a;新质生产力以新发展理念为思想指引&#xff0c;驱动数字经济创新发展动力、推动区域协调发展、转变发展方式、拓宽国际市场、共享数据要素&#xff0c;为数字经济高质量发展提供强大 动力支持。数字经济在发展过程中可能会存在关键性技术创新能力不足、传统…

Oracle视频基础1.4.3练习

15个视频 1.4.3 できない dbca删除数据库 id ls cd cd dbs ls ls -l dbca# delete a database 勾选 # chris 勾选手动删除数据库 ls ls -l ls -l cd /u01/oradata ls cd /u01/admin/ ls cd chris/ ls clear 初始化参数文件&#xff0c;admin&#xff0c;数据文件#新版本了…

.net core 接口,动态接收各类型请求的参数

[HttpPost] public async Task<IActionResult> testpost([FromForm] object info) { //Postman工具测试结果&#xff1a; //FromBody,Postman的body只有rawjson时才进的来 //参数为空时&#xff0c;Body(form-data、x-www-form-urlencoded)解析到的数据也有所…

《JVM第5课》虚拟机栈

无痛快速学习入门JVM&#xff0c;欢迎订阅本免费专栏 Java虚拟机栈&#xff08;Java Virtual Machine Stack&#xff0c;简称JVM栈&#xff0c;又称Java方法栈&#xff09;是 JVM 运行时数据区的一部分&#xff0c;主要用于支持Java方法的执行。每当一个新线程被创建时&#xf…

Axure大屏可视化模板:赋能各行各业的数据展示与管理

如何高效、直观地展示和分析数据&#xff0c;成为企业和机构面临的重要挑战。Axure大屏可视化模板作为一种先进的数据展示工具&#xff0c;凭借其强大的交互性和直观性&#xff0c;在多个领域内得到了广泛应用。从农业生产的智能化管理到城市发展的精细化管理&#xff0c;再到企…