SC-Lego-LOAM解析(下)

2023-05-16

回环检测是一个相对独立的模块,这里再开一篇专门说明。

前面两篇已经说过,先对点云做了预处理,然后进行连续帧之间的匹配即激光odom,然后是scan-to-map匹配,并保存关键帧的位姿,点云和ScanContext信息,这个过程中是存在这持续的误差积累的,因为没有进行回环检测,而进行回环检测的一个重要信息是ScanContext,这里来说明如何生成ScanContext,怎么检测回环以及如何进行优化。

生成ScanContext的函数其实在上一篇已经提及了,即void mapOptimization::saveKeyFramesAndFactor() 函数中有一个scManager.makeAndSaveScancontextAndKeys(*thisRawCloudKeyFrame);

下面主要分析这个函数是如何生成ScanContext,以及ScanContext到底包含了啥?

void SCManager::makeAndSaveScancontextAndKeys(pcl::PointCloud<SCPointType> &_scan_down) {
        // jin: 行为ring,列为sector,生成ScanContext
        // jin: make separate concentric rings
        Eigen::MatrixXd sc = makeScancontext(_scan_down); // v1
        // jin: 取一个ring的均值
        // jin: create keys from concentric rings
        Eigen::MatrixXd ringkey = makeRingkeyFromScancontext(sc);
        // jin: 取同心扇形区域的均值
        // jin: create keys from sectors
        Eigen::MatrixXd sectorkey = makeSectorkeyFromScancontext(sc);
        std::vector<float> polarcontext_invkey_vec = eig2stdvec(ringkey);

        polarcontexts_.push_back(sc);
        polarcontext_invkeys_.push_back(ringkey);
        polarcontext_vkeys_.push_back(sectorkey);
        polarcontext_invkeys_mat_.push_back(polarcontext_invkey_vec);

        // cout <<polarcontext_vkeys_.size() << endl;

    } 

3D点云在xy两个维度上可以以极坐标表示,我们在径向上可以分割可以划分出很多同心圆环ring,在360圆周上进行分割可以划分出很多同心扇形sector,两者交叉形成的就是很多个同心半圆环,每个区域用该区域所有点云高度的最大值来表示,这就是ScanContext,它将3D点云压缩成了二维的矩阵;
makeRingkeyFromScancontext是将每一个完整的ring用一个数字来代替,进而用一个向量来描述整个点云,方法就是,对于一个完整ring上不同sector区域内的值取均值即可;
与此类似makeSectorkeyFromScancontext是想用一个数字来表述整个sector,方法就是将同一个sector内不同ring内的值取均值。
到这里就将一帧完整的关键帧点云,压缩成了ScanContext这种二维矩阵,以及Ringkey和Sectorkey这两种向量。

回环的检测和全局位姿的优化是在mapOptmization的main函数中单独开了一个线程进行的:

std::thread loopthread(&lego_loam::mapOptimization::loopClosureThread, &MO);
看一看具体的代码:

void mapOptimization::loopClosureThread() {
        if (loopClosureEnableFlag == false)
            return;

        ros::Rate rate(1);
        while (ros::ok()) {
            rate.sleep();
            performLoopClosure();
        }
    } // loopClosureThread

以一定频率循环调用performLoopClosure,再来看看performLoopClosure:

void mapOptimization::performLoopClosure(void) {
        if (cloudKeyPoses3D->points.empty() == true)
            return;

        // jin: detect
        // try to find close key frame if there are any
        if (potentialLoopFlag == false) {
            // 分别根据距离和SCANCONTEXT信息查找回环帧,回环信息保存在成员变量中,包括回环帧的ID,点云,偏航角等
            if (detectLoopClosure() == true) {
                std::cout << std::endl;
                potentialLoopFlag = true; // find some key frames that is old enough or close enough for loop closure
                timeSaveFirstCurrentScanForLoopClosure = timeLaserOdometry;
            }
            if (potentialLoopFlag == false) {// jin: ScanContext未能找到可以形成回环的关键帧
                return;
            }
        }

        // reset the flag first no matter icp successes or not
        potentialLoopFlag = false;

        // 如果当前关键帧与历史关键帧确实形成了回环,开始进行优化
        // *****
        // Main
        // *****
        // make common variables at forward
        float x, y, z, roll, pitch, yaw;
        Eigen::Affine3f correctionCameraFrame;
        float noiseScore = 0.5; // constant is ok...
        gtsam::Vector Vector6(6);
        Vector6 << noiseScore, noiseScore, noiseScore, noiseScore, noiseScore, noiseScore;
        constraintNoise = noiseModel::Diagonal::Variances(Vector6);
        robustNoiseModel = gtsam::noiseModel::Robust::Create(
                gtsam::noiseModel::mEstimator::Cauchy::Create(1), // optional: replacing Cauchy by DCS or GemanMcClure
                gtsam::noiseModel::Diagonal::Variances(Vector6)
        ); // - checked it works. but with robust kernel, map modification may be delayed (i.e,. requires more true-positive loop factors)

        bool isValidRSloopFactor = false;
        bool isValidSCloopFactor = false;

        /*
         * 1. RS loop factor (radius search)
         * 将RS检测到的历史帧和当前帧匹配,求transform, 作为约束边
         */
        if (RSclosestHistoryFrameID != -1) {
            pcl::IterativeClosestPoint<PointType, PointType> icp;
            icp.setMaxCorrespondenceDistance(100);
            icp.setMaximumIterations(100);
            icp.setTransformationEpsilon(1e-6);
            icp.setEuclideanFitnessEpsilon(1e-6);
            icp.setRANSACIterations(0);

            // Align clouds
            icp.setInputSource(RSlatestSurfKeyFrameCloud);
            icp.setInputTarget(RSnearHistorySurfKeyFrameCloudDS);
            pcl::PointCloud<PointType>::Ptr unused_result(new pcl::PointCloud<PointType>());
            icp.align(*unused_result);
            // 上面比较的两个点云都已经被投影到了世界坐标系下,所以匹配的结果应该是这段时间内,原点所发生的漂移

            //  通过score阈值判定icp是否匹配成功
            std::cout << "[RS] ICP fit score: " << icp.getFitnessScore() << std::endl;
            if (icp.hasConverged() == false || icp.getFitnessScore() > historyKeyframeFitnessScore) {
                std::cout << "[RS] Reject this loop (bad icp fit score, > " << historyKeyframeFitnessScore << ")"
                          << std::endl;
                isValidRSloopFactor = false;
            } else {
                std::cout << "[RS] The detected loop factor is added between Current [ " << latestFrameIDLoopCloure
                          << " ] and RS nearest [ " << RSclosestHistoryFrameID << " ]" << std::endl;
                isValidRSloopFactor = true;
            }

            // jin: 最新帧与回环帧前后一定时间范围内的点组成的地图进行匹配,得到的坐标变换为最新帧与回环帧之间的约束
            // 因为作为地图的帧在回环帧前后很小的范围内,位姿变化很小,可以认为他们之间的相对位姿很准,地图也很准
            //  这里RS检测成功,加入约束边
            if (isValidRSloopFactor == true) {
                correctionCameraFrame = icp.getFinalTransformation(); // get transformation in camera frame (because points are in camera frame)
                pcl::getTranslationAndEulerAngles(correctionCameraFrame, x, y, z, roll, pitch, yaw);
                Eigen::Affine3f correctionLidarFrame = pcl::getTransformation(z, x, y, yaw, roll, pitch);
                //  transform from world origin to wrong pose
                // 最新关键帧在地图坐标系中的坐标,在过程中会存在误差的积累,否则匹配的结果必然是E
                // 这种误差可以被解释为地图原点发生了漂移
                Eigen::Affine3f tWrong = pclPointToAffine3fCameraToLidar(
                        cloudKeyPoses6D->points[latestFrameIDLoopCloure]);
                //  transform from world origin to corrected pose
                // 地图原点的漂移×在漂移后的地图中的坐标=没有漂移的坐标,即在回环上的关键帧时刻其应该所处的位姿
                // 这样就把当前帧的位姿转移到了回环关键帧所在时刻,没有漂移的情况下的位姿,两者再求解相对位姿
                /// 感觉以上很复杂,一开始完全没有把点云往世界坐标系投影啊!直接匹配不就是相对位姿么?
                Eigen::Affine3f tCorrect =
                        correctionLidarFrame * tWrong; // pre-multiplying -> successive rotation about a fixed frame
                pcl::getTranslationAndEulerAngles(tCorrect, x, y, z, roll, pitch, yaw);
                gtsam::Pose3 poseFrom = Pose3(Rot3::RzRyRx(roll, pitch, yaw), Point3(x, y, z));
                gtsam::Pose3 poseTo = pclPointTogtsamPose3(cloudKeyPoses6D->points[RSclosestHistoryFrameID]);
                gtsam::Vector Vector6(6);

                std::lock_guard<std::mutex> lock(mtx);
                gtSAMgraph.add(
                        BetweenFactor<Pose3>(latestFrameIDLoopCloure, RSclosestHistoryFrameID, poseFrom.between(poseTo),
                                             robustNoiseModel));
                isam->update(gtSAMgraph);
                isam->update();
                gtSAMgraph.resize(0);
            }
        }

//        /*
//         * 2. SC loop factor (scan context)
//         * SC检测成功,进行icp匹配
//         */
//        if (SCclosestHistoryFrameID != -1) {
//            pcl::IterativeClosestPoint<PointType, PointType> icp;
//            icp.setMaxCorrespondenceDistance(100);
//            icp.setMaximumIterations(100);
//            icp.setTransformationEpsilon(1e-6);
//            icp.setEuclideanFitnessEpsilon(1e-6);
//            icp.setRANSACIterations(0);
//
//            // Align clouds
//            // Eigen::Affine3f icpInitialMatFoo = pcl::getTransformation(0, 0, 0, yawDiffRad, 0, 0); // because within cam coord: (z, x, y, yaw, roll, pitch)
//            // Eigen::Matrix4f icpInitialMat = icpInitialMatFoo.matrix();
//            icp.setInputSource(SClatestSurfKeyFrameCloud);
//            icp.setInputTarget(SCnearHistorySurfKeyFrameCloudDS);
//            pcl::PointCloud<PointType>::Ptr unused_result(new pcl::PointCloud<PointType>());
//            icp.align(*unused_result);
//            // icp.align(*unused_result, icpInitialMat); // PCL icp non-eye initial is bad ... don't use (LeGO LOAM author also said pcl transform is weird.)
//
//            std::cout << "[SC] ICP fit score: " << icp.getFitnessScore() << std::endl;
//            if (icp.hasConverged() == false || icp.getFitnessScore() > historyKeyframeFitnessScore) {
//                std::cout << "[SC] Reject this loop (bad icp fit score, > " << historyKeyframeFitnessScore << ")"
//                          << std::endl;
//                isValidSCloopFactor = false;
//            } else {
//                std::cout << "[SC] The detected loop factor is added between Current [ " << latestFrameIDLoopCloure
//                          << " ] and SC nearest [ " << SCclosestHistoryFrameID << " ]" << std::endl;
//                isValidSCloopFactor = true;
//            }
//
//            // icp匹配成功也加入约束边
//            if (isValidSCloopFactor == true) {
//                correctionCameraFrame = icp.getFinalTransformation(); // get transformation in camera frame (because points are in camera frame)
//                pcl::getTranslationAndEulerAngles(correctionCameraFrame, x, y, z, roll, pitch, yaw);
//                gtsam::Pose3 poseFrom = Pose3(Rot3::RzRyRx(roll, pitch, yaw), Point3(x, y, z));
//                gtsam::Pose3 poseTo = Pose3(Rot3::RzRyRx(0.0, 0.0, 0.0), Point3(0.0, 0.0, 0.0));
//
//                std::lock_guard<std::mutex> lock(mtx);
//                // gtSAMgraph.add(BetweenFactor<Pose3>(latestFrameIDLoopCloure, closestHistoryFrameID, poseFrom.between(poseTo), constraintNoise)); // original
//                gtSAMgraph.add(
//                        BetweenFactor<Pose3>(latestFrameIDLoopCloure, SCclosestHistoryFrameID, poseFrom.between(poseTo),
//                                             robustNoiseModel)); // giseop
//                isam->update(gtSAMgraph);
//                isam->update();
//                gtSAMgraph.resize(0);
//            }
//        }

        // just for visualization
        // // publish corrected cloud
//         if (pubIcpKeyFrames.getNumSubscribers() != 0){
//             pcl::PointCloud<PointType>::Ptr closed_cloud(new pcl::PointCloud<PointType>());
//             pcl::transformPointCloud (*latestSurfKeyFrameCloud, *closed_cloud, icp.getFinalTransformation());
//             sensor_msgs::PointCloud2 cloudMsgTemp;
//             pcl::toROSMsg(*closed_cloud, cloudMsgTemp);
//             cloudMsgTemp.header.stamp = ros::Time().fromSec(timeLaserOdometry);
//             cloudMsgTemp.header.frame_id = "/camera_init";
//             pubIcpKeyFrames.publish(cloudMsgTemp);
//         }

        // flagging
        aLoopIsClosed = true;// jin: 在correctPoses中会导致对后端保存的pose位姿进行修改

    } // performLoopClosure

这个函数比较长,先说一下总体思路再详细看:

**首先detectLoopClosure() 找到回环信息,并保存在成员变量中,然后根据形成回环的帧的点云进行匹配建立当前最新关键帧与历史关键帧之间的约束,添加到gtsam进行图优化。**有个小细节是gtsam并没有立即根据优化后的位姿修改关键帧的位姿cloudKeyPoses3D,优化的最后只是对标志位aLoopIsClosed进行了修改,这个标志位的作用体现在主线程调用correctPoses()时将gtsam对位姿的优化同步出来。

下面重点说明的是detectLoopClosure()这个函数,其作用是根据距离和ScanContext信息确定与当前帧形成回环的历史关键帧,这里才真正体现了ScanContext的作用。

// jin: 检测与最新帧可以形成回环的关键帧,两种方法:
// 1/ 根据几何距离,在一定半径范围内,30s以上,最早的那个帧
// 2/ ScanContext,确定相似度最高的关键帧
bool mapOptimization::detectLoopClosure() {

std::lock_guard<std::mutex> lock(mtx);// 和void mapOptimization::run()不同时进行

/*
 * 邻域搜索闭环
 * 1. xyz distance-based radius search (contained in the original LeGO LOAM code)
 * - for fine-stichting trajectories (for not-recognized nodes within scan context search)
 */
// jin: 基于目前位姿,在一定范围内(20m)内搜索最近邻,若最早的那个超过了30s,则选中为回环目标
// 选取前后25帧组成点云,并保存当前最近一帧点云
RSlatestSurfKeyFrameCloud->clear();// 当前关键帧
RSnearHistorySurfKeyFrameCloud->clear();// 尝试进行回环的关键帧前后一定范围帧组成的点云
RSnearHistorySurfKeyFrameCloudDS->clear();// 上面的降采样

// find the closest history key frame
std::vector<int> pointSearchIndLoop;  //  搜索完的邻域点对应的索引
std::vector<float> pointSearchSqDisLoop;  //  搜索完的邻域点与当前点的欧氏距离
// 用当前的所有关键帧生成树
kdtreeHistoryKeyPoses->setInputCloud(cloudKeyPoses3D);
// currentRobotPosPoint为最新一帧关键帧的位姿
//  0:返回的邻域个数,为0表示返回全部的邻域点
kdtreeHistoryKeyPoses->radiusSearch(currentRobotPosPoint, historyKeyframeSearchRadius, pointSearchIndLoop,
                                    pointSearchSqDisLoop, 0);

// jin: 选取最近邻中,时间距离30s以上,最早的那帧
RSclosestHistoryFrameID = -1;
int curMinID = 1000000;
// policy: take Oldest one (to fix error of the whole trajectory)
for (int i = 0; i < pointSearchIndLoop.size(); ++i) {
    int id = pointSearchIndLoop[i];
    //  时间差值大于30s, 认为是闭环
    if (abs(cloudKeyPoses6D->points[id].time - timeLaserOdometry) > 30.0) {
        // RSclosestHistoryFrameID = id;
        // break;
        if (id < curMinID) {
            curMinID = id;
            RSclosestHistoryFrameID = curMinID;
        }
    }
}
if (RSclosestHistoryFrameID == -1) {
    // Do nothing here
    // then, do the next check: Scan context-based search
    // not return false here;
} else {
    // jin: 回环检测的进程是单独进行的,因此这里需要确定最新帧
    //  检测到回环了会保存四种点云
    // save latest key frames
    latestFrameIDLoopCloure = cloudKeyPoses3D->points.size() - 1;
    // jin: 根据当前的位姿,对点云进行旋转和平移
    // 点云的xyz坐标进行坐标系变换(分别绕xyz轴旋转)
    *RSlatestSurfKeyFrameCloud += *transformPointCloud(cornerCloudKeyFrames[latestFrameIDLoopCloure],
                                                       &cloudKeyPoses6D->points[latestFrameIDLoopCloure]);
    *RSlatestSurfKeyFrameCloud += *transformPointCloud(surfCloudKeyFrames[latestFrameIDLoopCloure],
                                                       &cloudKeyPoses6D->points[latestFrameIDLoopCloure]);
    // latestSurfKeyFrameCloud中存储的是下面公式计算后的index(intensity):
    // thisPoint.intensity = (float)rowIdn + (float)columnIdn / 10000.0;
    // 滤掉latestSurfKeyFrameCloud中index<0的点??? index值会小于0?
    pcl::PointCloud<PointType>::Ptr RShahaCloud(new pcl::PointCloud<PointType>());
    int cloudSize = RSlatestSurfKeyFrameCloud->points.size();
    for (int i = 0; i < cloudSize; ++i) {
        if ((int) RSlatestSurfKeyFrameCloud->points[i].intensity >= 0) {
            RShahaCloud->push_back(RSlatestSurfKeyFrameCloud->points[i]);
        }
    }
    RSlatestSurfKeyFrameCloud->clear();
    *RSlatestSurfKeyFrameCloud = *RShahaCloud;

    // jin: 保存一定范围内最早的那帧前后25帧的点,并在对应位姿处投影后进行合并
    // save history near key frames
    // historyKeyframeSearchNum在utility.h中定义为25,前后25个点进行变换
    for (int j = -historyKeyframeSearchNum; j <= historyKeyframeSearchNum; ++j) {
        if (RSclosestHistoryFrameID + j < 0 || RSclosestHistoryFrameID + j > latestFrameIDLoopCloure)
            continue;
        // 要求closestHistoryFrameID + j在0到cloudKeyPoses3D->points.size()-1之间,不能超过索引
        *RSnearHistorySurfKeyFrameCloud += *transformPointCloud(
                cornerCloudKeyFrames[RSclosestHistoryFrameID + j],
                &cloudKeyPoses6D->points[RSclosestHistoryFrameID + j]);
        *RSnearHistorySurfKeyFrameCloud += *transformPointCloud(surfCloudKeyFrames[RSclosestHistoryFrameID + j],
                                                                &cloudKeyPoses6D->points[
                                                                        RSclosestHistoryFrameID + j]);
    }
    //  下采样
    downSizeFilterHistoryKeyFrames.setInputCloud(RSnearHistorySurfKeyFrameCloud);
    downSizeFilterHistoryKeyFrames.filter(*RSnearHistorySurfKeyFrameCloudDS);
}

/*
 * 2. Scan context-based global localization
 */
SClatestSurfKeyFrameCloud->clear();
SCnearHistorySurfKeyFrameCloud->clear();
SCnearHistorySurfKeyFrameCloudDS->clear();

// std::lock_guard<std::mutex> lock(mtx);
latestFrameIDLoopCloure = cloudKeyPoses3D->points.size() - 1;
SCclosestHistoryFrameID = -1; // init with -1

// jin: 这是最重要的部分,根据ScanContext确定回环的关键帧,返回的是关键帧的ID,和yaw角的偏移量
auto detectResult = scManager.detectLoopClosureID(); // first: nn index, second: yaw diff
SCclosestHistoryFrameID = detectResult.first;
yawDiffRad = detectResult.second; // not use for v1 (because pcl icp withi initial somthing wrong...)

// if all close, reject
if (SCclosestHistoryFrameID == -1) {
    return false;
}

// 以下,如果SC检测到了回环,保存回环上的帧前后25帧和当前帧,过程与上面完全一样
// save latest key frames: query ptcloud (corner points + surface points)
// NOTE: using "closestHistoryFrameID" to make same root of submap points to get a direct relative between
// the query point cloud (latestSurfKeyFrameCloud) and the map (nearHistorySurfKeyFrameCloud). by giseop
// i.e., set the query point cloud within mapside's local coordinate
// jin: 将最新一帧激光点在回环位姿处进行投影???
*SClatestSurfKeyFrameCloud += *transformPointCloud(cornerCloudKeyFrames[latestFrameIDLoopCloure],
                                                   &cloudKeyPoses6D->points[SCclosestHistoryFrameID]);
*SClatestSurfKeyFrameCloud += *transformPointCloud(surfCloudKeyFrames[latestFrameIDLoopCloure],
                                                   &cloudKeyPoses6D->points[SCclosestHistoryFrameID]);

pcl::PointCloud<PointType>::Ptr SChahaCloud(new pcl::PointCloud<PointType>());
int cloudSize = SClatestSurfKeyFrameCloud->points.size();
for (int i = 0; i < cloudSize; ++i) {
    if ((int) SClatestSurfKeyFrameCloud->points[i].intensity >= 0) {
        SChahaCloud->push_back(SClatestSurfKeyFrameCloud->points[i]);
    }
}
SClatestSurfKeyFrameCloud->clear();
*SClatestSurfKeyFrameCloud = *SChahaCloud;

// jin: ScanContext确定的回环关键帧,前后一段范围内的点组成点云地图
// save history near key frames: map ptcloud (icp to query ptcloud)
for (int j = -historyKeyframeSearchNum; j <= historyKeyframeSearchNum; ++j) {
    if (SCclosestHistoryFrameID + j < 0 || SCclosestHistoryFrameID + j > latestFrameIDLoopCloure)
        continue;
    *SCnearHistorySurfKeyFrameCloud += *transformPointCloud(cornerCloudKeyFrames[SCclosestHistoryFrameID + j],
                                                            &cloudKeyPoses6D->points[SCclosestHistoryFrameID +
                                                                                     j]);
    *SCnearHistorySurfKeyFrameCloud += *transformPointCloud(surfCloudKeyFrames[SCclosestHistoryFrameID + j],
                                                            &cloudKeyPoses6D->points[SCclosestHistoryFrameID +
                                                                                     j]);
}
downSizeFilterHistoryKeyFrames.setInputCloud(SCnearHistorySurfKeyFrameCloud);
downSizeFilterHistoryKeyFrames.filter(*SCnearHistorySurfKeyFrameCloudDS);

// // optional: publish history near key frames
// if (pubHistoryKeyFrames.getNumSubscribers() != 0){
//     sensor_msgs::PointCloud2 cloudMsgTemp;
//     pcl::toROSMsg(*nearHistorySurfKeyFrameCloudDS, cloudMsgTemp);
//     cloudMsgTemp.header.stamp = ros::Time().fromSec(timeLaserOdometry);
//     cloudMsgTemp.header.frame_id = "/camera_init";
//     pubHistoryKeyFrames.publish(cloudMsgTemp);
// }

return true;

} // detectLoopClosure

检测回环的方法有两个:
1: //根据距离(RangeSearch)
2: //根据ScanContext

对于前者,寻找回环的条件可以概括为在物理空间上在一定范围内(这里是20m),时间上相差一定间隔(这里是30S)的所有帧中最早的那个,然后将前后25帧加起来形成一个局部map,两者之间就形成了回环,也是需要添加约束的两个对象,这里不再赘述,下面重点说一下根据ScanContext检测回环的方法。

从上面的代码中可以看出ScanContext的总体流程也差不多,都是寻找一个回环帧,然后把前后25个帧加起来作为回环的对象。不同之处在于,前者是根据关键帧的位姿在kd-tree中搜索出来的最近邻,后者是根据ScanContext信息,这是在函数scManager.detectLoopClosureID()中实现的:

// jin: 根据ScanContext确定回环的关键帧
// 先根据ring key向量,在kd-tree中搜索出多个位置相似的关键帧
// 然后对每个ScanContext列偏移多次,计算最好的列偏移及对应的距离,计算匹配度最好的关键帧的ID及其列偏移量
// 其中,为了加快对列偏移计算,用到了SectorKey先确定一个粗略的初始值
std::pair<int, float> SCManager::detectLoopClosureID(void) {
int loop_id{-1}; // init with -1, -1 means no loop (== LeGO-LOAM’s variable “closestHistoryFrameID”)
// jin: 最新一帧的ring key
auto curr_key = polarcontext_invkeys_mat_.back(); // current observation (query) 容器末尾元素
// jin: 最新一帧的Scan Context
auto curr_desc = polarcontexts_.back(); // current observation (query)

/*
 * step 1: candidates from ringkey tree_
 */
// jin: 之所以使用ring key是因为,ring代表了一周,即便角度相差很大对应ring的key(均值)也相差不大
// 因而,可以起到筛选处距离不会太远的关键帧的作用(圆环之间的相似度很高)
if (polarcontext_invkeys_mat_.size() < NUM_EXCLUDE_RECENT + 1) {
    std::pair<int, float> result{loop_id, 0.0};
    return result; // Early return
}

// 每间隔一定帧数才重新确定一次最近邻
// tree_ reconstruction (not mandatory to make everytime)
if (tree_making_period_conter % TREE_MAKING_PERIOD_ == 0) // to save computation cost
{
    TicToc t_tree_construction;

    polarcontext_invkeys_to_search_.clear();
    polarcontext_invkeys_to_search_.assign(polarcontext_invkeys_mat_.begin(),
                                           polarcontext_invkeys_mat_.end() - NUM_EXCLUDE_RECENT);// 最近一定范围内的不参加搜索吧
    // jin: 搜索树
    polarcontext_tree_.reset();
    /// 使用std::vector中的元素创建的TREE,方便搜索向量的最近邻
    polarcontext_tree_ = std::make_unique<InvKeyTree>(PC_NUM_RING /* dim */, polarcontext_invkeys_to_search_,
                                                      10 /* max leaf */ );
    // tree_ptr_->index->buildIndex(); // inernally called in the constructor of InvKeyTree (for detail, refer the nanoflann and KDtreeVectorOfVectorsAdaptor)
    t_tree_construction.toc("Tree construction");
}
tree_making_period_conter = tree_making_period_conter + 1;

double min_dist = 10000000; // init with somthing large
int nn_align = 0;
int nn_idx = 0;

// jin: 结果保存在这里
// knn search
std::vector<size_t> candidate_indexes(NUM_CANDIDATES_FROM_TREE);
std::vector<float> out_dists_sqr(NUM_CANDIDATES_FROM_TREE);

TicToc t_tree_search;
nanoflann::KNNResultSet<float> knnsearch_result(NUM_CANDIDATES_FROM_TREE);
knnsearch_result.init(&candidate_indexes[0], &out_dists_sqr[0]);
polarcontext_tree_->index->findNeighbors(knnsearch_result, &curr_key[0] /* query */,
                                         nanoflann::SearchParams(10));
t_tree_search.toc("Tree search");

/*
 *  step 2: pairwise distance (find optimal columnwise best-fit using cosine distance)
 */
// jin: 对上一步筛选出来的所有位姿,计算不同列偏移下的最好匹配度
TicToc t_calc_dist;
for (int candidate_iter_idx = 0; candidate_iter_idx < NUM_CANDIDATES_FROM_TREE; candidate_iter_idx++) {
    // jin: 候选帧的ScanContext
    MatrixXd polarcontext_candidate = polarcontexts_[candidate_indexes[candidate_iter_idx]];
    // jin: 这里也是一个重点
    // 根据不同列偏移,计算两帧之间匹配程度,返回的是最小差距和最好的偏移,这表征着两个ScanContext最终的差异
    std::pair<double, int> sc_dist_result = distanceBtnScanContext(curr_desc, polarcontext_candidate);

    double candidate_dist = sc_dist_result.first;
    int candidate_align = sc_dist_result.second;

    if (candidate_dist < min_dist) {
        min_dist = candidate_dist;// 最好帧的距离
        nn_align = candidate_align;// 最好帧的最好角度偏移量

        nn_idx = candidate_indexes[candidate_iter_idx];// 最好帧的索引
    }
}
t_calc_dist.toc("Distance calc");

/*
 * loop threshold check
 */
// 如果某帧经过最优偏移后的距离足够小
if (min_dist < SC_DIST_THRES) {
    loop_id = nn_idx;

    // std::cout.precision(3);
    cout << "[Loop found] Nearest distance: " << min_dist << " btn " << polarcontexts_.size() - 1 << " and "
         << nn_idx << "." << endl;
    cout << "[Loop found] yaw diff: " << nn_align * PC_UNIT_SECTORANGLE << " deg." << endl;
} else {
    std::cout.precision(3);
    cout << "[Not loop] Nearest distance: " << min_dist << " btn " << polarcontexts_.size() - 1 << " and "
         << nn_idx
         << "." << endl;
    cout << "[Not loop] yaw diff: " << nn_align * PC_UNIT_SECTORANGLE << " deg." << endl;
}

// To do: return also nn_align (i.e., yaw diff)
float yaw_diff_rad = deg2rad(nn_align * PC_UNIT_SECTORANGLE);
std::pair<int, float> result{loop_id, yaw_diff_rad};// jin: 返回匹配最优的KeyFrame的ID和计算出来的角度差

return result;

} // SCManager::detectLoopClosureID

这里先将所有关键帧的ring key信息生成一个tree,然后在树中搜索一定数量的最近邻。可以想想一下为什么是ring key以及ring key为什么可以。对于位置和姿态都未知的历史关键帧,如果其角度与当前帧角度存在较大差异,那么点云差别会非常大,这是由于雷达扫描机理造成的,而径向即便存在比较大的差距点云的相似度也会非常大。所以,如果某个历史关键帧位置比较接近,但是角度相差很大,那么ring的相似度依然会很高,这样就在不知道角度偏差的情况下,鲁棒的检测到相似帧。

但是仅仅如此还是不够的,很有可能错误检测到回环,而且不知道初始的角度差别, ICP也没法计算约束,因此还需要进一步估算方向的偏差,并从所有的候选相似帧中选出一个最优的。

distanceBtnScanContext函数即是计算当前ScanContext与所有候选历史关键帧ScanContext之间匹配度的函数,也即计算两个矩阵之间的相似度。该函数中为了快速确定最优的列偏移量和减小计算量,还调用了makeSectorkeyFromScancontext来将ScanContext压缩成向量先进行粗略的比较,再将整个ScanContext进行对比和计算相似度。

到这里通过两种不同的方式找出了当前最新关键帧与历史关键帧之间存在的回环,performLoopClosure中接下来的工作就是对回环的对象通过ICP计算约束,添加到gtsam并进行全局位姿优化。

到这里,SC-Lego-LOAM的分析基本就结束了。实际上有很多的细节可以分析,不再具体展开了,有些内容笔者也不太明白,比如说多次进行的坐标的交换怎么对应,gtsam的使用等(倒是常用Ceres…),以后慢慢学习,大家有合适的资源也欢迎留言推荐。

参考:
SC-Lego-LOAM解析(上)
SC-Lego-LOAM解析(中)
SC-Lego-LOAM解析(下)

本文经允许后转自知乎:https://zhuanlan.zhihu.com/p/348281520

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

SC-Lego-LOAM解析(下) 的相关文章

  • LOAM livox论文学习笔记

    Loam livox A fast robust high precision LiDAR odometry and mapping package for LiDARs of small FoV 参考 xff1a 多激光雷达的协同定位建图
  • LEGO-LOAM(LOAM)部分公式推导---未完待续

    一 featureAssociation相关推导 1 xff09 帧间匹配雅可比矩阵推导 首先明确LEGO LOAM中 xff0c 运动坐标系 xff08 符合右手系 xff09 的设置为 xff1a 因此对于平面运动来说 xff0c 影响
  • SLAM会议笔记(一)LOAM

    LOAM Lidar Odometry and Mapping in Real time ABSTRACT 将复杂的SLAM问题分离为两个算法 xff0c 一个高频低精度的运动估计 xff0c 另一个低一个数量级的点云匹配和配准算法 REL
  • SLAM会议笔记(三)V-LOAM

    Visual lidar Odometry and Mapping Low drift Robust and Fast Abstract 提出了一种新的使用激光雷达里程计和视觉里程计的框架 xff0c 提升了表现 xff0c 特别是在剧烈运
  • SLAM会议笔记(四)Lego-LOAM

    LeGO LOAM Lightweight and Ground Optimized Lidar Odometry and Mapping on Variable Terrain Abstract 提出一种轻量级的ground optimi
  • loam中imu消除重力加速度的数学推导

    最近在看loam的源码发现里面有一段关于imu消除重力加速度的源码 xff0c 刚开始看不明白后来终于搞清楚了 xff0c 欢迎大家批评指正 要理解这个问题首先得明白欧拉角到旋转矩阵的变换 先上图 此图描述的是先绕X xff0c 再绕Y x
  • 在用robosense的rslidar(16线)运行lego-loam遇到的问题总结

    在将rslidar通过github上开源的工具包转换成velodyne的点云格式后 运行测试lego loam时遇到了关于kdtree的报错问提 pcl KdTreeFLANN setInputCloud Cannot create a K
  • A-LOAM源码阅读

    LOAM 论文地址 xff1a https www ri cmu edu pub files 2014 7 Ji LidarMapping RSS2014 v8 pdf A LOAM地址 xff1a https github com HKU
  • LeGo-LOAM 跑通与源码学习

    论文链接 xff1a https www researchgate net LeGO LOAM 源码仓库 xff1a https github com RobustFieldAutonomyLab LeGO LOAM 本人注释 xff1a
  • velodyne运行Loam_velodyne过程记录

    刚拿到手的3D激光雷达 xff0c 运行一下试试 xff08 1 xff09 loam velodyne环境配置 cd catkin ws src git clone https github com laboshinl loam velo
  • A-LOAM学习

    A LOAM学习 一 复现1 1 Ubuntu 和 ROS1 2 Ceres Solver1 3 PCL 二 下载A LOAM三 下数据集 一 复现 1 1 Ubuntu 和 ROS A LOAM 1 2 Ceres Solver span
  • A-LOAM学习

    A LOAM学习 一 kittiHelper cpp二 scanRegistration cpp三 laserOdometry cpp四 laserMapping cpp 一 kittiHelper cpp 本代码旨在实现 将kitti数据
  • LOAM进行点云地图创建

    3D激光点云数据处理入门 xff08 一 xff09 使用LOAM进行点云地图创建 LOAM 原理简述topic关系算法分析算法伪代码 LOAM 建图实践创建你的 ROS Workspace下载LOAM Package下载数据包运行 LOA
  • SC-Lego-LOAM解析(上)

    文章目录 正文imageProjectionfeatureAssociationFeature Extraction 正文 SC Lego LOAM实际上应该并不对应某一篇特定的论文 xff0c 而是韩国KAIST在github开源的代码
  • SC-Lego-LOAM解析(中)

    上回说到经过连续帧间匹配 xff0c 激光odo给出来一个位姿估计 xff0c 但是是存在不断的误差的积累的 xff0c 需要与绝对的参考 xff08 地图 xff09 进行匹配 xff0c 以及进行回环检测和全局位姿优化 这也是正是map
  • SC-Lego-LOAM解析(下)

    回环检测是一个相对独立的模块 xff0c 这里再开一篇专门说明 前面两篇已经说过 xff0c 先对点云做了预处理 xff0c 然后进行连续帧之间的匹配即激光odom xff0c 然后是scan to map匹配 xff0c 并保存关键帧的位
  • 保存并查看Lego-Loam的三维点云地图

    Loam的安装及运行方法可以参考 https blog csdn net qq 36396941 article details 82973772 本文提供ROS wiki http wiki ros org loam velodyne上无
  • LOAM_velodyne学习(三)

    终于到第三个模块了 我们先来回顾下之前的工作 点云数据进来后 经过前两个节点的处理可以完成一个完整但粗糙的里程计 可以概略地估计出Lidar的相对运动 如果不受任何测量噪声的影响 这个运动估计的结果足够精确 没有任何漂移 那我们可以直接利用
  • 使用C/C++编程控制LEGO EV3

    环境搭建 1 安装Eclipse 选择Eclipse IDE for C C Developers 网址 http www eclipse org downloads 2 安装c4ev3 网址 https c4ev3 github io 该
  • 利用python语言编程控制LEGO EV3

    1 环境搭建 安装WinSCP 网址 https sourceforge net projects winscp 下载一个自己喜欢的python IDE 例如pycharm 将EV3的系统换为ev3dev 参考网址 https www ev

随机推荐

  • github下载加速的几种方法

    文章目录 1 github加速的几种办法1 1 把github的代码 xff0c 转到码云上1 2 有人做了github的代下载网站 xff0c 可以从上面进行下载1 3 使用cnpmjs镜像进行加速1 4 使用国外服务器进行搭桥 2 总结
  • EasyLogger的代码解读和移植(linux和stm)

    文章目录 1 EasyLogger目录结构分析 2 EasyLogger之docs查看总结 2 1 EasyLogger之docs查看 2 1 2 api gt kernel md文档 2 1 3 port gt kernel md文档 2
  • C++之socket.io编译使用

    文章目录 1 什么是socket io2 开发环境配置2 1 获取socket io的源码2 2 cmake安装2 3 boost安装2 3 1 获取源码2 3 2 解压编译下载 2 4 rapidjson下载2 5 websocketpp
  • github push的改版

    1 记录一次github 推送时的错误 错误如下 xff1a remote Please see https github blog 2020 12 15 token authentication requirements for git
  • Frp内网穿透

    Frp内网穿透 所有经过服务器的内网穿透都是有一个服务端和客户端 因为都需要借助服务器的公网ip来访问进而达到内网穿透的效果 frp的github开源地址 https github com fatedier frp frp的说明文档 htt
  • linux常见的几种排序方法

    我们以数组a 61 2 6 8 9 1 2 进行排序输出作为列子 xff1a 下面我来总结几种方法来帮助大家学习 1 xff1a 常规排序 首先2和6对比 xff0c 2不比6大不因此不交换 xff0c 所以还是268912 xff0c 然
  • 值得推荐的C/C++框架和库

    值得学习的C语言开源项目 Libevent libev是一个开源的事件驱动库 xff0c 基于epoll xff0c kqueue等OS提供的基础设施 其以高效出名 xff0c 它可以将IO事件 xff0c 定时器 xff0c 和信号统一起
  • cmake教程4(find_package使用)

    本文主要内容如下 xff1a 1 cmake find package的基本原理 2 如何编写自己的 cmake module模块 3 使用cmake find package 使用不同版本的opencv lib问题 xff08 openc
  • A*算法

    如此好贴 xff0c 不能不转 xff01 原文地址 xff1a http dev gameres com Program Abstract Arithmetic AmitAStar mht 本文版权归原作者 译者所有 xff0c 我只是转
  • 3.5RC_Channel 和 SRV_channel

    前言 这部分是之前在折腾ROVER的时候梳理了一遍 xff0c 但是没有形成记录 xff0c 现在从新从对象的角度来分析一遍 xff0c 包括映射 转换 数据结构 等方面进行梳理 xff1b 数据结构 首先分析的入口是从Rover read
  • CAN通信详解

    本章我们将向大家介绍如何使用STM32自带的CAN控制器来实现两个开发板之间的CAN通讯 xff0c 并将结果显示在TFTLCD模块上 本章分为如下几个部分 xff1a 30 1 CAN简介 30 2 硬件设计 30 3 软件设计 30 4
  • VC中出现的一些小问题的解决办法

    本人用的是vc6 0的 xff0c 在实际调试过程中出现一些小的问题 xff0c 通过网上查询资料得以解决 xff0c 特此在此整理下 xff0c 方便后者参考 第一问题 xff1a 程序源码编写完成后编译没有问题 xff0c 而链接时出现
  • C++编译报错`XXX‘被多次定义总结;未定义的引用等等

    1 C 43 43 编译报错 96 XXX 被多次定义总结 报错原因 xff1a 诸如类似的报错都是因为可能在两个或者多个 cpp文件 h文件定义该全局变量 xff0c 属于重复定义问题 解决办法是 xff1a 在VScode全局搜索该变量
  • C++必备万能头文件“#include<bits/stdc++.h>”

    c 43 43 代码一个简单的头文件 xff1a span class token macro property span class token directive hash span span class token directive
  • Livox SLAM(带LIO+闭环检测优化)

    主题 xff1a Livox雷达LIO 43 闭环检测优化 开源地址 xff1a LiDAR SLAM 该开源为 Livox雷达实现了一个一体化且即用型的 LiDAR 惯性里程计系统 前端基于基于开源里程计框架LIO Livox获取里程计信
  • 大疆livox定制的格式CustomMsg格式转换pointcloud2

    官方livox driver驱动livox雷达发出的点云topic有两种 xff0c 一种是大疆览沃定制的格式CustomMsg格式 xff0c 另一种是将CustomMsg格式 转换过的pointcloud2格式 xff0c 参见 Liv
  • paddlepaddle

    项目用到了paddlespeech2 xff0c 学了几天paddlepaddle xff0c 简单记录一下 文章目录 1 手写数字识别任务2 极简方案构建手写数字识别模型模型设计训练配置训练过程模型测试 3 手写数字识别 之数据处理4 手
  • SC-Lego-LOAM解析(上)

    文章目录 正文imageProjectionfeatureAssociationFeature Extraction 正文 SC Lego LOAM实际上应该并不对应某一篇特定的论文 xff0c 而是韩国KAIST在github开源的代码
  • SC-Lego-LOAM解析(中)

    上回说到经过连续帧间匹配 xff0c 激光odo给出来一个位姿估计 xff0c 但是是存在不断的误差的积累的 xff0c 需要与绝对的参考 xff08 地图 xff09 进行匹配 xff0c 以及进行回环检测和全局位姿优化 这也是正是map
  • SC-Lego-LOAM解析(下)

    回环检测是一个相对独立的模块 xff0c 这里再开一篇专门说明 前面两篇已经说过 xff0c 先对点云做了预处理 xff0c 然后进行连续帧之间的匹配即激光odom xff0c 然后是scan to map匹配 xff0c 并保存关键帧的位