Karto_slam可以说第一次把因子图优化内容用于slam当中,是具有划时代意义的开源激光slam系统。 它的主要贡献有两个:(1)提出了一个效果更好的扫描匹配过程; (2)利用观测信息、位姿关系构建因子图,求解最佳回环估计。
参考:Karto_slam框架与代码解析 Karto SLAM算法学习
整体来说,看代码的话,karto代码框架还是比较整洁的,唯一的不好就是把多个类都放在同一个文件下了,造成代码特别庞大,尤其是Mapper.cpp文件,其基本上包含了所有算法核心。 在karto_slam文件夹下,主要关注两个:slam_karto.cpp和spa_solver.cpp; 在open_karto文件夹下,主要关注src文件夹中的Mapper.cpp,其中include里的Mapper.h有地图查找模板的具体实现。 同样的,重要的函数就是标红的函数:MatchScan()和TryCloseLoop()。 KartoSLAM是基于图优化的方法,用高度优化和非迭代 cholesky矩阵进行稀疏系统解耦作为解,图优化方法利用图的均值表示地图,每个节点表示机器人轨迹的一个位置点和传感器测量数据集,箭头的指向的连接表示连续机器人位置点的运动,每个新节点加入,地图就会依据空间中的节点箭头的约束进行计算更新.
匹配的方式是scanTomap,兴趣区域就是矩形形状的submap,也可以将这块区域理解为参考模型。 以里程计估计的位置为中心的一个矩形区域,用以表示最终位置的可能范围,在匹配时,遍历搜索区域,获取响应值最高的位置。 对于激光获得的数据信息,以一定的角分辨率和角偏移值进行投影,获取查找表,用以匹配。
kt_double ScanMatcher::MatchScan(LocalizedRangeScan* pScan, const LocalizedRangeScanVector& rBaseScans, Pose2& rMean, Matrix3& rCovariance, kt_bool doPenalize, kt_bool doRefineMatch) { // set scan pose to be center of grid1. 得到激光帧的传感器位姿 Pose2 scanPose = pScan->GetSensorPose(); // scan has no readings; cannot do scan matching // best guess of pose is based off of adjusted odometer reading //扫描没有读数,不能做扫描匹配 //姿势的最佳猜测是基于调整后的里程表读数 if (pScan->GetNumberOfRangeReadings() == 0) { rMean = scanPose; // maximum covariance rCovariance(0, 0) = MAX_VARIANCE; // XX rCovariance(1, 1) = MAX_VARIANCE; // YY rCovariance(2, 2) = 4 * math::Square(m_pMapper->m_pCoarseAngleResolution->GetValue()); // TH*TH return 0.0; } // 2. 得到栅格的尺寸 Rectangle2<kt_int32s> roi = m_pCorrelationGrid->GetROI(); // 3. 计算偏移 (in meters - lower left corner) Vector2<kt_double> offset; offset.SetX(scanPose.GetX() - (0.5 * (roi.GetWidth() - 1) * m_pCorrelationGrid->GetResolution())); offset.SetY(scanPose.GetY() - (0.5 * (roi.GetHeight() - 1) * m_pCorrelationGrid->GetResolution())); // 4. 设置偏移 m_pCorrelationGrid->GetCoordinateConverter()->SetOffset(offset); // 设置相关性栅格 AddScans(rBaseScans, scanPose.GetPosition()); // compute how far to search in each direction计算每个方向上的搜索距离 Vector2<kt_double> searchDimensions(m_pSearchSpaceProbs->GetWidth(), m_pSearchSpaceProbs->GetHeight()); Vector2<kt_double> coarseSearchOffset(0.5 * (searchDimensions.GetX() - 1) * m_pCorrelationGrid->GetResolution(), 0.5 * (searchDimensions.GetY() - 1) * m_pCorrelationGrid->GetResolution()); // a coarse search only checks half the cells in each dimension粗糙搜索只检查每一维的一半栅格 Vector2<kt_double> coarseSearchResolution(2 * m_pCorrelationGrid->GetResolution(), 2 * m_pCorrelationGrid->GetResolution()); // actual scan-matching实际的扫描匹配 kt_double bestResponse = CorrelateScan(pScan, scanPose, coarseSearchOffset, coarseSearchResolution, m_pMapper->m_pCoarseSearchAngleOffset->GetValue(), m_pMapper->m_pCoarseAngleResolution->GetValue(), doPenalize, rMean, rCovariance, false); if (m_pMapper->m_pUseResponseExpansion->GetValue() == true) { if (math::DoubleEqual(bestResponse, 0.0)) { // try and increase search angle offset with 20 degrees and do another match //尝试增加20度的搜索角度偏移并进行另一次匹配 kt_double newSearchAngleOffset = m_pMapper->m_pCoarseSearchAngleOffset->GetValue(); for (kt_int32u i = 0; i < 3; i++) { newSearchAngleOffset += math::DegreesToRadians(20); bestResponse = CorrelateScan(pScan, scanPose, coarseSearchOffset, coarseSearchResolution, newSearchAngleOffset, m_pMapper->m_pCoarseAngleResolution->GetValue(), doPenalize, rMean, rCovariance, false); if (math::DoubleEqual(bestResponse, 0.0) == false) { break; } } } } if (doRefineMatch) {//进行精细匹配 Vector2<kt_double> fineSearchOffset(coarseSearchResolution * 0.5); Vector2<kt_double> fineSearchResolution(m_pCorrelationGrid->GetResolution(), m_pCorrelationGrid->GetResolution()); bestResponse = CorrelateScan(pScan, rMean, fineSearchOffset, fineSearchResolution, 0.5 * m_pMapper->m_pCoarseAngleResolution->GetValue(), m_pMapper->m_pFineSearchAngleOffset->GetValue(), doPenalize, rMean, rCovariance, true); } assert(math::InRange(rMean.GetHeading(), -KT_PI, KT_PI)); return bestResponse; }回环检测的操作和添加邻近边类似,步骤较为繁琐: 1、依据当前的节点, 从Graph中找到与之相邻的所有节点(一定距离范围内)
2、采取广度优先搜索的方式,将相邻(next)与相连(adjacentVertices)添加进nearLinkedScans.
3、从sensorManager中取从前到后,依据id序号挑选与当前在一定距离范围内,且不在nearLinkedScans中的candidateScans, 当数量达到一定size,返回。
4、loopScanMatcher进行scanTomap的匹配,当匹配response 和covariance达到一定要求认为闭环检测到。得到调整的correct pose。
5、Add link to loop : 调整边(全局闭环)
6、触发correctPose: spa优化
kt_bool MapperGraph::TryCloseLoop(LocalizedRangeScan* pScan, const Name& rSensorName) { kt_bool loopClosed = false; kt_int32u scanIndex = 0; //寻找可能与当前帧发生闭环的观测帧 LocalizedRangeScanVector candidateChain = FindPossibleLoopClosure(pScan, rSensorName, scanIndex); while (!candidateChain.empty()) { Pose2 bestPose; Matrix3 covariance; kt_double coarseResponse = m_pLoopScanMatcher->MatchScan(pScan, candidateChain, bestPose, covariance, false, false);//进行扫描匹配 m_pMapper->FireLoopClosureCheck(stream.str()); if ((coarseResponse > m_pMapper->m_pLoopMatchMinimumResponseCoarse->GetValue()) && (covariance(0, 0) < m_pMapper->m_pLoopMatchMaximumVarianceCoarse->GetValue()) && (covariance(1, 1) < m_pMapper->m_pLoopMatchMaximumVarianceCoarse->GetValue())) { LocalizedRangeScan tmpScan(pScan->GetSensorName(), pScan->GetRangeReadingsVector()); tmpScan.SetUniqueId(pScan->GetUniqueId()); tmpScan.SetTime(pScan->GetTime()); tmpScan.SetStateId(pScan->GetStateId()); tmpScan.SetCorrectedPose(pScan->GetCorrectedPose()); tmpScan.SetSensorPose(bestPose); // This also updates OdometricPose. kt_double fineResponse = m_pMapper->m_pSequentialScanMatcher->MatchScan(&tmpScan, candidateChain, bestPose, covariance, false);//再次扫描匹配 m_pMapper->FireLoopClosureCheck(stream1.str()); if (fineResponse < m_pMapper->m_pLoopMatchMinimumResponseFine->GetValue()) m_pMapper->FireLoopClosureCheck("REJECTED!"); else { m_pMapper->FireBeginLoopClosure("Closing loop..."); pScan->SetSensorPose(bestPose); LinkChainToScan(candidateChain, pScan, bestPose, covariance); CorrectPoses();//执行优化计算,修正位姿 m_pMapper->FireEndLoopClosure("Loop closed!"); loopClosed = true; } } candidateChain = FindPossibleLoopClosure(pScan, rSensorName, scanIndex); } return loopClosed; }