回环检测 Scan Contex 及 扩展(二)

前文介绍了基于Scan Context扩展的SC-LIO-SAM,本文将对FAST_LIO_SLAM进行介绍。

1.先总结SC-LIO-SAM相比原代码增加的内容

  • 保存关键帧的scan context描述子;
  • LIO-SAM是基于时间和距离判断回环帧的,SC-LIO-SAM新增通过scan context寻找回环帧的方法(当前帧和回环帧点云局部点云ICP,计算位姿),加入因子图。

2.为什么加回环:

参考作者在知乎[1]回复:“fast lio 定位是前端,类似 svo,因为我们认为学术工作应当专注在创新点上,而不在于功能是否多。回环检测和回环优化我们都有独立的研究发表了。如果只是简单的将回环加入 lio,这个偏工程应用,不太适合作为学术工作发表。”

3.FAST_LIO_SLAM:

在FAST-LIO2的基础上,添加SC-PGO模块,通过加入ScanContext全局描述子,进行回环修正

SC-PGO (Loop detection and Pose-graph Optimization): Scan Context-based Loop detection and GTSAM-based Pose-graph optimization。

总结FAST-LIO本身是一个非常棒的前端(里程计)FAST_LIO_SLAM增加了回环检测和后端优化,变为完整的SLAM。

4.Code:

代码很好的一点是 SC-PGO模块与FAST-LIO2解耦,SC-PGO 从 FAST-LIO2 节点接收里程计和激光点云的话题。

1.run FAST-LIO2 
roslaunch fast_lio mapping_ouster64_mulran.launch

2.run SC-PGO
roslaunch aloam_velodyne fastlio_ouster64.launch

根据launch文件,只关注laserPosegraphOptimization.cpp代码即可

4.1订阅:

  • 订阅fast-lio 发布的"/cloud_registered_body"  ,这个点云是去畸变点云,注意是imu坐标系,不是lidar坐标系,回调函数把点云数据放到fullResBuf
  • 订阅fast-lio 发布的  "/Odometry",位姿是imu到map的变换,回调函数把odom数据放到odometryBuf

4.2线程:

1.process_pg 构建位姿图(Pose Graph)主线程

主要流程:

  • 等待点云+里程计数据到达、对齐;
  • 计算关键帧间的相对运动,判断当前帧是否为关键帧(超过一定距离/角度);
  • 是关键帧则保存点云、pose,向pose graph添加先验因子和里程计因子;
  • 同步保存当前帧点云为PCD、时间戳等;
void process_pg()
{
    while(1)
    {
		while ( !odometryBuf.empty() && !fullResBuf.empty() )
        {

			mBuf.lock();   
            // (1) 如果 odometry 时间比 fullRes 时间早,则 odometry 过时,丢弃    
            while (!odometryBuf.empty() && odometryBuf.front()->header.stamp.toSec() < fullResBuf.front()->header.stamp.toSec())
                odometryBuf.pop();

            if (odometryBuf.empty())
            {
                mBuf.unlock();
                break;
            }

            // (2) 取当前帧时间戳
            timeLaserOdometry = odometryBuf.front()->header.stamp.toSec();
            timeLaser = fullResBuf.front()->header.stamp.toSec();
            
            // (3) 取出点云数据,转换为pcl格式
            pcl::PointCloud<PointType>::Ptr thisKeyFrame(new pcl::PointCloud<PointType>());
            pcl::fromROSMsg(*fullResBuf.front(), *thisKeyFrame);
            fullResBuf.pop();

            // (4) 取出当前odom位姿
            Pose6D pose_curr = getOdom(odometryBuf.front());
            odometryBuf.pop();

            double eps = 0.1; // find a gps topioc arrived within eps second 
            while (!gpsBuf.empty()) {
                auto thisGPS = gpsBuf.front();
                auto thisGPSTime = thisGPS->header.stamp.toSec();
                if( abs(thisGPSTime - timeLaserOdometry) < eps ) {
                    currGPS = thisGPS;
                    hasGPSforThisKF = true; 
                    break;
                } else {
                    hasGPSforThisKF = false;
                }
                gpsBuf.pop();
            }
            mBuf.unlock(); 

            odom_pose_prev = odom_pose_curr;
            odom_pose_curr = pose_curr;
            Pose6D dtf = diffTransformation(odom_pose_prev, odom_pose_curr); // 位姿增量
            double delta_translation = sqrt(dtf.x*dtf.x + dtf.y*dtf.y + dtf.z*dtf.z); // 平移增量
            translationAccumulated += delta_translation;
            rotaionAccumulated += (dtf.roll + dtf.pitch + dtf.yaw); // 累计欧拉角增量

            // (5) 判断是否为关键帧
            // 第一帧是关键帧
            if( translationAccumulated > keyframeMeterGap || rotaionAccumulated > keyframeRadGap ) {
                // 满足位移或转角要求,标记为关键帧
                isNowKeyFrame = true;
                translationAccumulated = 0.0; // reset 
                rotaionAccumulated = 0.0; // reset 
            } else {
                isNowKeyFrame = false;
            }

            if( ! isNowKeyFrame ) 
                continue; 

            if( !gpsOffsetInitialized ) {
                if(hasGPSforThisKF) { // if the very first frame 
                    gpsAltitudeInitOffset = currGPS->altitude;
                    gpsOffsetInitialized = true;
                } 
            }

            // (6) 记录关键帧点云、位姿、时间,构建scancontext描述子
            pcl::PointCloud<PointType>::Ptr thisKeyFrameDS(new pcl::PointCloud<PointType>());
            downSizeFilterScancontext.setInputCloud(thisKeyFrame);
            downSizeFilterScancontext.filter(*thisKeyFrameDS);

            mKF.lock(); 
            keyframeLaserClouds.push_back(thisKeyFrameDS);  // 保存关键帧点云
            keyframePoses.push_back(pose_curr);             // 保存关键帧位姿
            keyframePosesUpdated.push_back(pose_curr);      // 后端优化后的位姿,初值就是当前
            keyframeTimes.push_back(timeLaserOdometry);     // 时间戳

            // 当前关键帧生成sc 描述子
            scManager.makeAndSaveScancontextAndKeys(*thisKeyFrameDS);

            mKF.unlock(); 

            // (7) 添加因子
            const int prev_node_idx = keyframePoses.size() - 2; 
            const int curr_node_idx = keyframePoses.size() - 1; // becuase cpp starts with 0 (actually this index could be any number, but for simple implementation, we follow sequential indexing)
            if( ! gtSAMgraphMade /* prior node */) {
                const int init_node_idx = 0; 
                gtsam::Pose3 poseOrigin = Pose6DtoGTSAMPose3(keyframePoses.at(init_node_idx));
                
                mtxPosegraph.lock();
                {
                    // 添加先验因子
                    gtSAMgraph.add(gtsam::PriorFactor<gtsam::Pose3>(init_node_idx, poseOrigin, priorNoise));
                    initialEstimate.insert(init_node_idx, poseOrigin);
                }   
                mtxPosegraph.unlock();

                gtSAMgraphMade = true; 
                cout << "posegraph prior node " << init_node_idx << " added" << endl;
            } else /* consecutive node (and odom factor) after the prior added */ { // == keyframePoses.size() > 1 
                gtsam::Pose3 poseFrom = Pose6DtoGTSAMPose3(keyframePoses.at(prev_node_idx));
                gtsam::Pose3 poseTo = Pose6DtoGTSAMPose3(keyframePoses.at(curr_node_idx));

                mtxPosegraph.lock();
                {
                    // 添加odom因子
                    gtSAMgraph.add(gtsam::BetweenFactor<gtsam::Pose3>(prev_node_idx, curr_node_idx, poseFrom.between(poseTo), odomNoise));

                    // gps factor 
                    if(hasGPSforThisKF) {
                        double curr_altitude_offseted = currGPS->altitude - gpsAltitudeInitOffset;
                        mtxRecentPose.lock();
                        gtsam::Point3 gpsConstraint(recentOptimizedX, recentOptimizedY, curr_altitude_offseted); // in this example, only adjusting altitude (for x and y, very big noises are set) 
                        mtxRecentPose.unlock();
                        gtSAMgraph.add(gtsam::GPSFactor(curr_node_idx, gpsConstraint, robustGPSNoise));
                        cout << "GPS factor added at node " << curr_node_idx << endl;
                    }
                    initialEstimate.insert(curr_node_idx, poseTo);                
                }
                mtxPosegraph.unlock();

                if(curr_node_idx % 100 == 0)
                    cout << "posegraph odom node " << curr_node_idx << " added." << endl;
            }
            // if want to print the current graph, use gtSAMgraph.print("\nFactor Graph:\n");

            // 保存关键帧点云和时间
            std::string curr_node_idx_str = padZeros(curr_node_idx);
            pcl::io::savePCDFileBinary(pgScansDirectory + curr_node_idx_str + ".pcd", *thisKeyFrame); // scan 
            pgTimeSaveStream << timeLaser << std::endl; // path 
        }

        // ps. 
        // scan context detector is running in another thread (in constant Hz, e.g., 1 Hz)
        // pub path and point cloud in another thread

        // wait (must required for running the while loop)
        std::chrono::milliseconds dura(2);
        std::this_thread::sleep_for(dura);
    }
} // process_pg

2. process_lcd:回环检测线程 (Loop Closure Detection)

用Scan Context进行回环检测,周期性判断当前帧和历史帧是否构成回环。

主要流程

  • 周期性(如1Hz)调用performSCLoopClosure(),检测当前关键帧是否与历史帧回环;
  • 检测到回环时,将回环帧对(索引)push到scLoopICPBuf
void performSCLoopClosure(void)
{
    if( int(keyframePoses.size()) < scManager.NUM_EXCLUDE_RECENT) // do not try too early 
        return;

    auto detectResult = scManager.detectLoopClosureID(); // first: nn index, second: yaw diff 
    int SCclosestHistoryFrameID = detectResult.first;
    if( SCclosestHistoryFrameID != -1 ) { 
        const int prev_node_idx = SCclosestHistoryFrameID;    // 回环帧id
        const int curr_node_idx = keyframePoses.size() - 1;   // 当前帧id
        cout << "Loop detected! - between " << prev_node_idx << " and " << curr_node_idx << "" << endl;

        mBuf.lock();
        // 回环帧和当前帧id放到队列中,等待后续ICP线程处理
        scLoopICPBuf.push(std::pair<int, int>(prev_node_idx, curr_node_idx));
        // addding actual 6D constraints in the other thread, icp_calculation.
        mBuf.unlock();
    }
} // performSCLoopClosure
// 回环检测线程
void process_lcd(void)
{
    float loopClosureFrequency = 1.0; // can change 
    ros::Rate rate(loopClosureFrequency);
    while (ros::ok())
    {
        rate.sleep();
        performSCLoopClosure();
    }
} // process_lcd

3. process_icp:回环对齐线程 (ICP计算)

主要流程

  • scLoopICPBuf队列取出回环帧对,当前帧点云和回环帧前后25帧组成的局部点云进行ICP(点云都是map系),得到当前帧和回环帧的位姿变换;
  • 向pose graph添加BetweenFactor(回环因子)。
void process_icp(void)
{
    while(1)
    {
		while ( !scLoopICPBuf.empty() )
        {
            if( scLoopICPBuf.size() > 30 ) {
                ROS_WARN("Too many loop clousre candidates to be ICPed is waiting ... Do process_lcd less frequently (adjust loopClosureFrequency)");
            }

            mBuf.lock(); 
            // 取出一个回环帧对(prev历史帧, curr当前帧)
            std::pair<int, int> loop_idx_pair = scLoopICPBuf.front();
            scLoopICPBuf.pop();
            mBuf.unlock(); 

            const int prev_node_idx = loop_idx_pair.first;
            const int curr_node_idx = loop_idx_pair.second;
            // 当前帧和回环帧的位姿变换
            auto relative_pose_optional = doICPVirtualRelative(prev_node_idx, curr_node_idx);
            if(relative_pose_optional) {
                gtsam::Pose3 relative_pose = relative_pose_optional.value();
                mtxPosegraph.lock();
                // 回环因子
                gtSAMgraph.add(gtsam::BetweenFactor<gtsam::Pose3>(prev_node_idx, curr_node_idx, relative_pose, robustLoopNoise));
                mtxPosegraph.unlock();
            } 
        }

        // wait (must required for running the while loop)
        std::chrono::milliseconds dura(2);
        std::this_thread::sleep_for(dura);
    }
} 

std::optional<gtsam::Pose3> doICPVirtualRelative( int _loop_kf_idx, int _curr_kf_idx )
{
    // parse pointclouds
    int historyKeyframeSearchNum = 25; // enough. ex. [-25, 25] covers submap length of 50x1 = 50m if every kf gap is 1m
    pcl::PointCloud<PointType>::Ptr cureKeyframeCloud(new pcl::PointCloud<PointType>());
    pcl::PointCloud<PointType>::Ptr targetKeyframeCloud(new pcl::PointCloud<PointType>());
    loopFindNearKeyframesCloud(cureKeyframeCloud, _curr_kf_idx, 0, _loop_kf_idx); // use same root of loop kf idx 
    loopFindNearKeyframesCloud(targetKeyframeCloud, _loop_kf_idx, historyKeyframeSearchNum, _loop_kf_idx); 

    // loop verification 
    sensor_msgs::PointCloud2 cureKeyframeCloudMsg;
    pcl::toROSMsg(*cureKeyframeCloud, cureKeyframeCloudMsg);
    cureKeyframeCloudMsg.header.frame_id = "camera_init";
    pubLoopScanLocal.publish(cureKeyframeCloudMsg);

    sensor_msgs::PointCloud2 targetKeyframeCloudMsg;
    pcl::toROSMsg(*targetKeyframeCloud, targetKeyframeCloudMsg);
    targetKeyframeCloudMsg.header.frame_id = "camera_init";
    pubLoopSubmapLocal.publish(targetKeyframeCloudMsg);

    // ICP Settings
    pcl::IterativeClosestPoint<PointType, PointType> icp;
    icp.setMaxCorrespondenceDistance(150); // giseop , use a value can cover 2*historyKeyframeSearchNum range in meter 
    icp.setMaximumIterations(100);
    icp.setTransformationEpsilon(1e-6);
    icp.setEuclideanFitnessEpsilon(1e-6);
    icp.setRANSACIterations(0);

    // Align pointclouds
    icp.setInputSource(cureKeyframeCloud);
    icp.setInputTarget(targetKeyframeCloud);
    pcl::PointCloud<PointType>::Ptr unused_result(new pcl::PointCloud<PointType>());
    icp.align(*unused_result);
 
    float loopFitnessScoreThreshold = 0.3; // user parameter but fixed low value is safe. 
    if (icp.hasConverged() == false || icp.getFitnessScore() > loopFitnessScoreThreshold) {
        std::cout << "[SC loop] ICP fitness test failed (" << icp.getFitnessScore() << " > " << loopFitnessScoreThreshold << "). Reject this SC loop." << std::endl;
        return std::nullopt;
    } else {
        std::cout << "[SC loop] ICP fitness test passed (" << icp.getFitnessScore() << " < " << loopFitnessScoreThreshold << "). Add this SC loop." << std::endl;
    }

    // Get pose transformation
    float x, y, z, roll, pitch, yaw;
    Eigen::Affine3f correctionLidarFrame;
    correctionLidarFrame = icp.getFinalTransformation();
    pcl::getTranslationAndEulerAngles (correctionLidarFrame, 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));

    return poseFrom.between(poseTo);
}

4. process_isam:优化线程(后端ISAM2)

作用:调用gtsam ISAM2优化器,对pose graph做全局优化,得到全局最优位姿。

void process_isam(void)
{
    float hz = 1; 
    ros::Rate rate(hz);
    while (ros::ok()) {
        rate.sleep();
        if( gtSAMgraphMade ) {    // 如果已经构建过因子图
            mtxPosegraph.lock();
            runISAM2opt();        // 调用一次isam2增量优化
            cout << "running isam2 optimization ..." << endl;
            mtxPosegraph.unlock();

            // 保存优化后(经过全局图优化)的轨迹结果到KITTI格式文本文件
            saveOptimizedVerticesKITTIformat(isamCurrentEstimate, pgKITTIformat);
            // 保存 原始里程计轨迹(未经过优化的位姿)到KITTI格式文本文件
            saveOdometryVerticesKITTIformat(odomKITTIformat); 
        }
    }
}

void runISAM2opt(void)
{
    // called when a variable added 
    isam->update(gtSAMgraph, initialEstimate);   // 用因子图和初值更新isam2内部图模型
    isam->update();                              // 增量优化内部状态
    
    gtSAMgraph.resize(0);
    initialEstimate.clear();

    isamCurrentEstimate = isam->calculateEstimate();  // 得到全局优化后位姿的最优解
    updatePoses();   // 更新位姿
}

5. process_viz_path:轨迹可视化线程

发布odom、轨迹以及tf,供RViz可视化。

6. process_viz_map:地图可视化线程

发布全局地图点云,供RViz可视化。

核心线程之间的关系

  • 主线程(process_pg)负责基础数据收集、构建pose graph;

  • 回环检测(process_lcd)不断检测历史回环,把候选回环送到ICP线程;

  • ICP(process_icp)计算回环位姿约束并添加到因子图;

  • ISAM2优化(process_isam)后端优化关键帧位姿;

  • 可视化(viz_path/map)线程实时给RViz等提供轨迹、地图。

参考:

1.https://zhuanlan.zhihu.com/p/1004403765

2.https://github.com/gisbi-kim/FAST_LIO_SLAM?tab=readme-ov-file

<think>好的,用户想了解如何在mid-360设备上使用Fast-LIO2进行建图和导航。首先,我需要回忆Fast-LIO2的基本信息。Fast-LIO2是一个高效的激光SLAM算法,适用于固态激光雷达如Livox MID-360。接下来,用户提到了建图和导航,所以需要分步骤解释这两个部分。 根据引用内容,用户需要安装ROS驱动和Fast-LIO2,可能需要参考引用中的步骤。比如引用[2]提到了安装ROS2驱动和Fast-LIO2的步骤,需要新建工作空间,编译代码等。然后运行建图可能需要启动相应的launch文件,如mapping_avia.launch。导航部分可能涉及与Move_Base的集成,如引用[1]提到的基于Fast_Lio和Move_Base搭建导航系统。 接下来需要整理步骤,确保逻辑清晰。可能需要先说明环境搭建,包括ROS安装、驱动安装、Fast-LIO2的编译。然后是建图的具体流程,包括启动驱动和建图节点。导航部分需要配置Move_Base和代价地图,可能需要引用中的优化方法,比如引用[3]提到的SC-PGO优化,提升稳定性。 用户可能遇到的问题包括驱动安装错误、参数配置不当、传感器标定等,需要提醒注意事项。同时,性能优化方面,参考引用[3],使用简化版代码或融合SC-PGO可能有助于稳定性。 最后,生成相关问题时要围绕Fast-LIO2的应用、优化、与其他算法的比较等,确保覆盖用户可能感兴趣的扩展内容。</think>### 使用 Fast-LIO2 在 Livox MID-360 上实现建图与导航的步骤 #### 一、环境搭建 1. **安装 ROS 与依赖** - 推荐 Ubuntu 18.04/20.04 + ROS Melodic/Noetic,需安装 `livox_ros_driver2` 驱动[^2]。 - 创建工作空间并克隆代码: ```bash mkdir -p fast_lio2_ws/src cd fast_lio2_ws/src git clone https://github.com/hku-mars/FAST_LIO.git ``` 2. **编译 Fast-LIO2** - 安装依赖项(如 Sophus、Eigen3)后编译: ```bash cd .. catkin_make source devel/setup.bash ``` 3. **配置 MID-360 驱动** - 将 `livox_ros_driver2` 放入 ROS 工程目录,并修改 `msg_MID360.launch` 中的设备 IP 和参数。 --- #### 、建图流程 1. **启动 Fast-LIO2 建图节点** ```bash roslaunch fast_lio mapping_avia.launch ``` - 需在 `avia.yaml` 中配置 MID-360 的激光雷达参数(如扫描频率、视场角)[^3]。 2. **启动 MID-360 驱动** ```bash roslaunch livox_ros_driver2 msg_MID360.launch ``` 3. **保存地图** - 建图完成后,使用 `pcl_ros` 保存点云地图: ```bash rosrun pcl_ros pointcloud_to_pcd input:=/cloud_registered ``` --- #### 三、导航集成(基于 Move_Base) 1. **配置代价地图与导航参数** - 在 `costmap_common_params.yaml` 中加载 Fast-LIO2 输出的点云或特征地图[^1]。 - 设置 `global_planner` 和 `local_planner` 的路径规划算法。 2. **启动导航节点** ```bash roslaunch move_base move_base.launch ``` - 需确保 Fast-LIO2 的里程计话题 `/odometry` 与导航系统对齐。 3. **优化稳定性(可选)** - 若出现漂移,可融合 SC-PGO 优化模块,通过订阅 Fast-LIO2 的里程计和点云进行后端优化。 --- #### 四、注意事项 1. **传感器标定** - MID-360 需预先完成 IMU 和激光雷达的外参标定,否则会导致轨迹偏移。 2. **参数调优** - 调整 `mapping_avia.launch` 中的 `max_iteration` 和 `filter_size_corner` 以平衡精度与速度。 3. **实时性保障** - 在资源受限设备上,可尝试 S-FAST-LIO 简化版,减少维流形计算量[^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值