从零开始搭二维激光SLAM --- 简单重写 Hector SLAM

2024-04-02 22:18

本文主要是介绍从零开始搭二维激光SLAM --- 简单重写 Hector SLAM,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

上篇文章讲解了如何在固定位置使用Hector构建单帧的栅格地图,以及知道了SLAM的本质就是将不同时刻的scan在正确的位置上写成栅格地图.

本篇文章将对 Hector 进行简单的重写,使得其代码更简单,更清晰.

这也是本系列教程第一次成功建出一张比较好的地图.

先放图,虽然有些瑕疵,但是整体还是不错的.
在这里插入图片描述
话不多说,先说明一下对代码做了那些改变,然后再着重讲解一下 Hector是如何做scan match的.

1 代码简单重构

相对于原版Hector做了哪些改变

  • 重写了原版的HectorMappingRos.cpp,使得代码变得更加简单与清晰
  • 发布了 map -> odom -> base_link 的 TF树
  • 去掉了原版的 DebugInfo 以及 Drawing 这两个功能以及代码文件.个人感觉这两个功能用不上,导致代码复杂
  • 更改了一些文件的位置,使得现在的.cc文件只有一个,其余全是头文件.原版的src文件夹下有很多文件
  • 对很多文件进行了注释

2 代码讲解

2.1 获取代码

代码已经提交在github上了,如果不知道github的地址的朋友, 请在我的公众号: 从零开始搭激光SLAM 中回复 开源地址 获得。

推荐使用 git clone 的方式进行下载, 因为代码是正处于更新状态的, git clone 下载的代码可以使用 git pull 很方便地进行更新.

本篇文章对应的代码为 Creating-2D-laser-slam-from-scratch/lesson4/src/hector_mapping/ 与 Creating-2D-laser-slam-from-scratch/lesson4/include/hector_mapping/hector_slam.cc

2.2 构造函数

在构造函数中 首先调用 InitParams 进行了 ROS参数的初始化.

之后对 HectorSlamProcessor 对象进行初始化.

再之后对多层地图进行了初始化,并调用 setMapInfo() 进行 ROS地图的内存的分配.

最后,查找从 base_link 到 雷达坐标系 间的坐标变换.

// 构造函数
HectorMappingRos::HectorMappingRos(): private_node_("~"), lastGetMapUpdateIndex(-100), tfB_(0), map_publish_thread_(0)
{ROS_INFO_STREAM("\033[1;32m----> Hector SLAM started.\033[0m");// 参数初始化InitParams();laser_scan_subscriber_ = node_handle_.subscribe(p_scan_topic_, p_scan_subscriber_queue_size_, &HectorMappingRos::scanCallback, this); // 雷达数据处理if (p_pub_odometry_){odometryPublisher_ = node_handle_.advertise<nav_msgs::Odometry>("odom_hector", 50);}tfB_ = new tf::TransformBroadcaster();slamProcessor = new hectorslam::HectorSlamProcessor(static_cast<float>(p_map_resolution_),p_map_size_, p_map_size_,Eigen::Vector2f(p_map_start_x_, p_map_start_y_),p_map_multi_res_levels_);slamProcessor->setUpdateFactorFree(p_update_factor_free_);                // 0.4slamProcessor->setUpdateFactorOccupied(p_update_factor_occupied_);        // 0.9slamProcessor->setMapUpdateMinDistDiff(p_map_update_distance_threshold_); // 0.4slamProcessor->setMapUpdateMinAngleDiff(p_map_update_angle_threshold_);   // 0.9// 多层地图的初始化int mapLevels = slamProcessor->getMapLevels();mapLevels = 1; // 这里设置成只发布最高精度的地图,如果有其他需求,如进行路径规划等等需要多层地图时,注释本行。std::string mapTopic_ = "map";for (int i = 0; i < mapLevels; ++i){mapPubContainer.push_back(MapPublisherContainer());slamProcessor->addMapMutex(i, new HectorMapMutex());std::string mapTopicStr(mapTopic_);if (i != 0){mapTopicStr.append("_" + boost::lexical_cast<std::string>(i));}std::string mapMetaTopicStr(mapTopicStr);mapMetaTopicStr.append("_metadata");MapPublisherContainer &tmp = mapPubContainer[i];tmp.mapPublisher_ = node_handle_.advertise<nav_msgs::OccupancyGrid>(mapTopicStr, 1, true);tmp.mapMetadataPublisher_ = node_handle_.advertise<nav_msgs::MapMetaData>(mapMetaTopicStr, 1, true);setMapInfo(tmp.map_, slamProcessor->getGridMap(i)); // 设置地图服务if (i == 0){mapPubContainer[i].mapMetadataPublisher_.publish(mapPubContainer[i].map_.map.info);}}// 新建一个线程用来发布地图map_publish_thread_ = new boost::thread(boost::bind(&HectorMappingRos::publishMapLoop, this, p_map_pub_period_));map_to_odom_.setIdentity();// 查找 base_link -> front_laser_link 的tf,循环5次以确保其能找到int count = 5;ros::Duration dur(0.5);while (count-- != 0){if (tf_.waitForTransform(p_base_frame_, p_scan_frame_, ros::Time(0), dur)){tf_.lookupTransform(p_base_frame_, p_scan_frame_, ros::Time(0), laserTransform_);break;}else{ROS_WARN("lookupTransform laser frame into base_link timed out.");}}
}

2.3 回调函数

首先进行了雷达数据格式的转换. 先通过 laser_geometry 这个包将 LaserScan 转换成 PointCloud 格式,这个很简单,我之前的文章已经实现过了,这里懒的改了.

之后是将雷达数据从点云形式转换到Hector自己的雷达数据存储格式.

再之后就是进行扫描匹配与地图更新.

扫描匹配计算完成之后再发布里程计 topic 与 tf,tf将根据不同的配置决定是发布 map -> base_link 还是 map -> odom -> base_link.


想要看输出的里程计信息 topic 就必须要有odom坐标系.

这里将 map 与 odom 的坐标系重合了( 没有对 map_to_odom_ 再赋值 ) ,所以 base_link 在 map 与 odom 下的坐标是相同的.

在发布TF的时候,将TF的时间戳都设置成了雷达的时间戳,这块好像有问题,会导致在rviz里 fixed frame 设置成map时看不到hector发布的odom数据。

我也将时间戳改成了 ros::Time::now() ,改完了之后有时能看到有时看不到,还不清楚原因,所以先在rviz里将 fixed frame 设置成了odom.

/*** 激光数据处理回调函数,将ros数据格式转换为算法中的格式,并转换成地图尺度,交由slamProcessor处理。* 算法中所有的计算都是在地图尺度下进行。  */
void HectorMappingRos::scanCallback(const sensor_msgs::LaserScan &scan)
{start_time_ = std::chrono::steady_clock::now();ros::WallTime startTime = ros::WallTime::now();// 将 scan 转换成 点云格式projector_.projectLaser(scan, laser_point_cloud_, 30.0);Eigen::Vector3f startEstimate(Eigen::Vector3f::Zero());// 将雷达数据的点云格式 更改成 hector 内部的数据格式if (rosPointCloudToDataContainer(laser_point_cloud_, laserTransform_, laserScanContainer, slamProcessor->getScaleToMap())){// 首先获取上一帧的位姿,作为初值startEstimate = slamProcessor->getLastScanMatchPose();// 进入扫描匹配与地图更新slamProcessor->update(laserScanContainer, startEstimate);}end_time_ = std::chrono::steady_clock::now();time_used_ = std::chrono::duration_cast<std::chrono::duration<double>>(end_time_ - start_time_);std::cout << "数据转换与扫描匹配用时: " << time_used_.count() << " 秒。" << std::endl;if (p_timing_output_){ros::WallDuration duration = ros::WallTime::now() - startTime;ROS_INFO("HectorSLAM Iter took: %f milliseconds", duration.toSec() * 1000.0f);}// 更新存储的位姿, 这里的位姿是 base_link 在 map 下的位姿poseInfoContainer_.update(slamProcessor->getLastScanMatchPose(), slamProcessor->getLastScanMatchCovariance(), scan.header.stamp, p_map_frame_);// 发布 odom topicif (p_pub_odometry_){nav_msgs::Odometry tmp;tmp.pose = poseInfoContainer_.getPoseWithCovarianceStamped().pose;tmp.header = poseInfoContainer_.getPoseWithCovarianceStamped().header;tmp.child_frame_id = p_base_frame_;odometryPublisher_.publish(tmp);}if (pub_map_to_baselink_tf_){// pub map -> odom -> base_link tfif (p_pub_map_odom_transform_){tfB_->sendTransform(tf::StampedTransform(map_to_odom_, scan.header.stamp, p_map_frame_, p_odom_frame_));tfB_->sendTransform(tf::StampedTransform(poseInfoContainer_.getTfTransform(), scan.header.stamp, p_odom_frame_, p_tf_map_scanmatch_transform_frame_name_));}// pub map -> base_link tfelse{tfB_->sendTransform(tf::StampedTransform(poseInfoContainer_.getTfTransform(), scan.header.stamp, p_map_frame_, p_tf_map_scanmatch_transform_frame_name_));}}
}

2.4 转换scan格式的函数

这里的 laserTransform 指的是从 base_link 到 雷达坐标系 间的坐标变换.

首先将base_link到雷达坐标系的坐标转换 乘以地图分辨率 设置为这帧数据的 origo.

之后对每个雷达数据进行距离判定,如果距离太近或者太远就跳过.

这里还进行了如果距离大于 雷达数据的最大使用距离 的判断,因为雷达数据太远时的数据跳动比较厉害,所以这里将 p_use_max_scan_range_ 之外的数据也过滤掉不进行使用.

之后就是将数据点进行坐标变换,通过左乘laserTransform得到在base_link下的正确的x y 值,因为只进行了一次左乘laserTransform,所以转换后的z坐标是不正确的,通过再次减去laserTransform才得到正确的z坐标.

最后将base_link下的xy乘以分辨率放入dataContainer中.

// 将点云数据转换成Hector中雷达数据的格式
bool HectorMappingRos::rosPointCloudToDataContainer(const sensor_msgs::PointCloud &pointCloud, const tf::StampedTransform &laserTransform, hectorslam::DataContainer &dataContainer, float scaleToMap)
{size_t size = pointCloud.points.size();dataContainer.clear();tf::Vector3 laserPos(laserTransform.getOrigin());// dataContainer.setOrigo(Eigen::Vector2f::Zero());// 将base_link到雷达坐标系的坐标转换 乘以地图分辨率 当成这帧数据的 origodataContainer.setOrigo(Eigen::Vector2f(laserPos.x(), laserPos.y()) * scaleToMap);for (size_t i = 0; i < size; ++i){const geometry_msgs::Point32 &currPoint(pointCloud.points[i]);float dist_sqr = currPoint.x * currPoint.x + currPoint.y * currPoint.y;if ((dist_sqr > p_sqr_laser_min_dist_) && (dist_sqr < p_sqr_laser_max_dist_)){if ((currPoint.x < 0.0f) && (dist_sqr < 0.50f)){continue;}// 距离太远的点跳动太大,如果距离大于使用距离(20m),就跳过if (dist_sqr > p_use_max_scan_range_ * p_use_max_scan_range_)continue;// 点的坐标左乘base_link->laser_link的tf 将得到该点在base_link下的 xy 坐标, 但是z坐标是不正确tf::Vector3 pointPosBaseFrame(laserTransform * tf::Vector3(currPoint.x, currPoint.y, currPoint.z));// 通过再减去 base_link->laser_link的tf的z的值,得到该点在base_link下正确的 z 坐标float pointPosLaserFrameZ = pointPosBaseFrame.z() - laserPos.z();if (pointPosLaserFrameZ > p_laser_z_min_value_ && pointPosLaserFrameZ < p_laser_z_max_value_){// 将雷达数据的 x y 都乘地图的分辨率 0.05 再放入dataContainer中dataContainer.add(Eigen::Vector2f(pointPosBaseFrame.x(), pointPosBaseFrame.y()) * scaleToMap);}}}return true;
}

2.5 update()

update() 函数位于 Creating-2D-laser-slam-from-scratch/lesson4/include/lesson4/hector_mapping/slam_main/HectorSlamProcessor.h中.

将scan_match与地图更新分成了2步进行.

scan_match是调用mapRep->matchData()函数,返回了当前scan在与地图进行扫描匹配后的地图坐标系下的位姿.

之后调用了updateByScan()进行了地图的更新,这块代码已经在上一篇文章中讲过了,所以这里就不再进行讲解了.

    /*** 对每一帧的激光数据进行处理* @param dataContainer  激光数据存储容器,坐标已转换成地图尺度,为地图中激光系的坐标* @param poseHintWorld  激光系在地图中的初始pose* @param map_without_matching 是否进行匹配*/void update(const DataContainer &dataContainer, const Eigen::Vector3f &poseHintWorld, bool map_without_matching = false){//std::cout << "\nph:\n" << poseHintWorld << "\n";/** 1. 位姿匹配 **/Eigen::Vector3f newPoseEstimateWorld;if (!map_without_matching){// 进行 scan to map 的地方newPoseEstimateWorld = (mapRep->matchData(poseHintWorld, dataContainer, lastScanMatchCov));}else{newPoseEstimateWorld = poseHintWorld;}lastScanMatchPose = newPoseEstimateWorld;/** 2.地图更新 **/if (util::poseDifferenceLargerThan(newPoseEstimateWorld, lastMapUpdatePose, paramMinDistanceDiffForMapUpdate, paramMinAngleDiffForMapUpdate) || map_without_matching){ // 仅在位姿变化大于阈值 或者 map_without_matching为真 的时候进行地图更新mapRep->updateByScan(dataContainer, newPoseEstimateWorld);mapRep->onMapUpdated();lastMapUpdatePose = newPoseEstimateWorld;}}

2.5.1 matchData()_1

第一个matchData() 位于 Creating-2D-laser-slam-from-scratch/lesson4/include/lesson4/hector_mapping/slam_main/MapRepMultiMap.h中.

假设一共3层多分辨率地图,这里首先使用 setFrom() 将雷达数据dataContainer进行长度的缩短,对每个原始数据都乘以 1/(2的n次方) , n是 2, 1, 0.

之后使用缩短后的雷达数据,与对应分辨率的地图进行扫描匹配.

也就是在粗分辨率地图上先进行扫描匹配,得出的结果 再传入 更精细分辨率地图上进行扫描匹配.

最终返回一个在map坐标系下的位姿.

    /*** 地图匹配,通过多分辨率地图求解当前激光帧的pose。*/virtual Eigen::Vector3f matchData(const Eigen::Vector3f &beginEstimateWorld, const DataContainer &dataContainer, Eigen::Matrix3f &covMatrix){size_t size = mapContainer.size();Eigen::Vector3f tmp(beginEstimateWorld);/// coarse to fine 的pose求精过程,i层的求解结果作为i-1层的求解初始值。for (int index = size - 1; index >= 0; --index){//std::cout << " m " << i;if (index == 0){tmp = (mapContainer[index].matchData(tmp, dataContainer, covMatrix, 5)); /// 输入数据dataContainer 对应 mapContainer[0]}else{// 根据地图层数对原始激光数据坐标进行缩小,得到对应图层尺寸的激光数据dataContainers[index - 1].setFrom(dataContainer, static_cast<float>(1.0 / pow(2.0, static_cast<double>(index))));tmp = (mapContainer[index].matchData(tmp, dataContainers[index - 1], covMatrix, 3));/// dataContainers[i]对应mapContainer[i+1]}}return tmp;}

2.5.2 matchData()_2

第二个machData() 位于 Creating-2D-laser-slam-from-scratch/lesson4/include/lesson4/hector_mapping/slam_main/MapProcContainer.h 中.

可以看到,这个函数中并没有额外的操作,只是将对应分辨率的地图也一起传入到第三个mathcData() 中.

    /*** 给定位姿初值,估计当前scan在当前图层中的位姿 ---- 位姿为世界系下的pose 、  dataContainer应与当前图层尺度匹配* @param beginEstimateWorld 世界系下的位姿* @param dataContainer       激光数据* @param covMatrix           scan-match的不确定性 -- 协方差矩阵* @param maxIterations       最大的迭代次数* @return*/Eigen::Vector3f matchData(const Eigen::Vector3f &beginEstimateWorld, const DataContainer &dataContainer, Eigen::Matrix3f &covMatrix, int maxIterations){return scanMatcher->matchData(beginEstimateWorld, *gridMapUtil, dataContainer, covMatrix, maxIterations);}

2.5.3 matchData()_3

第三个 matchData() 位于 Creating-2D-laser-slam-from-scratch/lesson4/include/lesson4/hector_mapping/matcher/ScanMatcher.h 中.

这回终于是真正进行扫描匹配的地方了.


首先说一下hector world坐标系与hector map坐标系

之前说的这个初始位姿一直都是world坐标系下的坐标,Hector中认为实际物理坐标表示成的坐标为world坐标,以左上角为原点的.

hector map坐标就是用 world坐标再除以地图的分辨率0.05(也就是乘以20),再加上map坐标系的原点 得到的.也就是像素坐标.


这段代码先将给定的初始位姿转换成Hector的map坐标系下的像素坐标.

之后调用了 estimateTransformationLogLh() 进行计算位姿,之后又通过多次调用这个函数使结果更加准确.

求解的过程就在 estimateTransformationLogLh() 里.

这里的H矩阵是这个类的私有变量,代表hessian, 也就是要返回的协方差矩阵.

然后,将estimate从地图坐标系下转到world坐标系下,再返回.

    /*** 实际进行位姿估计的函数* @param beginEstimateWorld  位姿初值* @param gridMapUtil         网格地图工具,这里主要是用来做坐标变换* @param dataContainer       激光数据* @param covMatrix           协方差矩阵* @param maxIterations       最大迭代次数* @return*/Eigen::Vector3f matchData(const Eigen::Vector3f &beginEstimateWorld,ConcreteOccGridMapUtil &gridMapUtil, const DataContainer &dataContainer,Eigen::Matrix3f &covMatrix, int maxIterations){if (dataContainer.getSize() != 0){// 1. 初始pose转换为地图尺度下的pose --仿射变换,包括位姿的尺度和偏移,旋转在 X Y 同尺度变化时保持不变Eigen::Vector3f beginEstimateMap(gridMapUtil.getMapCoordsPose(beginEstimateWorld));Eigen::Vector3f estimate(beginEstimateMap);// 2. 第一次迭代estimateTransformationLogLh(estimate, gridMapUtil, dataContainer);int numIter = maxIterations;/** 3. 多次迭代求解 **/for (int i = 0; i < numIter; ++i){estimateTransformationLogLh(estimate, gridMapUtil, dataContainer);}// 角度正则化estimate[2] = util::normalize_angle(estimate[2]);covMatrix = Eigen::Matrix3f::Zero();//covMatrix.block<2,2>(0,0) = (H.block<2,2>(0,0).inverse());//covMatrix.block<2,2>(0,0) = (H.block<2,2>(0,0));// 使用Hessian矩阵近似协方差矩阵covMatrix = H;// 结果转换回物理坐标系下 -- 转换回实际尺度return gridMapUtil.getWorldCoordsPose(estimate);}return beginEstimateWorld;}

2.6 estimateTransformationLogLh()

这个函数也位于 Creating-2D-laser-slam-from-scratch/lesson4/include/lesson4/hector_mapping/matcher/ScanMatcher.h 中.

首先,这个函数通过getCompleteHessianDerivs() 计算出了协方差矩阵H , 以及 dTr向量.

之后,通过高斯牛顿法最终推倒出的求根公式: H的逆 乘以 dtr 求出 先验位姿与后验位姿间的增量. 也就是 X = H.inverse() * dTr --> H * X = dTr

最后再将这个增量加上之前的预测值,得到匹配之后的位姿.

    /***  高斯牛顿估计位姿* @param estimate      位姿初始值* @param gridMapUtil   网格地图相关计算工具* @param dataPoints    激光数据* @return  提示是否有解 --- 貌似没用上*/bool estimateTransformationLogLh(Eigen::Vector3f &estimate,ConcreteOccGridMapUtil &gridMapUtil,const DataContainer &dataPoints){/** 核心函数,计算H矩阵和dTr向量(b列向量)---- occGridMapUtil.h 中 **/gridMapUtil.getCompleteHessianDerivs(estimate, dataPoints, H, dTr);//std::cout << "\nH\n" << H  << "\n";//std::cout << "\ndTr\n" << dTr  << "\n";// 判断H是否可逆, 判断增量非0,避免无用计算if ((H(0, 0) != 0.0f) && (H(1, 1) != 0.0f)){// 求解位姿增量Eigen::Vector3f searchDir(H.inverse() * dTr);// 角度增量不能太大if (searchDir[2] > 0.2f){searchDir[2] = 0.2f;std::cout << "SearchDir angle change too large\n";}else if (searchDir[2] < -0.2f){searchDir[2] = -0.2f;std::cout << "SearchDir angle change too large\n";}/// 更新估计值 --- 结果在地图尺度下updateEstimatedPose(estimate, searchDir);return true;}return false;}void updateEstimatedPose(Eigen::Vector3f &estimate, const Eigen::Vector3f &change){estimate += change;}

由于篇幅限制,getCompleteHessianDerivs() 函数的讲解与hector的公式推倒将在下篇文章中进行说明

3 运行

3.1 运行

本篇文章对应的数据包, 请在我的公众号中回复 lesson1 获得,并将launch中的bag_filename更改成您实际的目录名。

通过如下命令运行本篇文章对应的程序

roslaunch lesson4 hector_slam.launch

因为hector中依赖了 laser_geometry 这个包,如果有同学启动失败,提示缺少这个包,手动安装一下就好了.

3.2 运行结果与分析

启动之后,会在rviz中显示出如下画面.

首先,要说明一点的是rviz中的 Fixed Frame 我设置成了odom,因为我map与odom是重合的,所以这块设置成odom也是可以的.

如果设置成map,odom_hector这块会显示不出来,会提示找不到 TF,这个原因我还不太清楚,猜测是发布map->odom的tf的时间戳设置成雷达的时间戳这块存在问题,但是我没有去深扣它.

Rviz的 Fixed Frame 的意义是:所有显示出来的信息,都要通过 TF 进行坐标变换,变换到Fixed Frame 下,如果在规定的时间内没有查到 TF 就会报错,显示不了信息.

所以我将Fixed Frame设置成odom后,显示的2个odometry插件就不再需要进行坐标变换,可以直接显示出来.
在这里插入图片描述红色轨迹是小车上的编码器的结果,蓝色轨迹是hector_slam输出的里程计结果.

通过在建图的过程中对比雷达与地图的匹配,以及对比红色蓝色的轨迹,可以发现,hector计算出的里程计还是挺准确的,即使是回来的过程中,雷达与地图也是始终匹配上的.

但是,在回来的过程中,编码器产生的里程计与hector的里程计产生了较大的偏差.

通过最后小车停止时,雷达数据与地图依然能够很好的匹配这一现象,可以认为hector产生的里程计的准确程度大于小车自身的里程计.

至于走廊边缘那边界不清晰的问题,通过观看建图过程发现,主要是由于远距离的雷达点跳动产生的.

计算耗时

同时,终端会打印处如下信息,可以看到,执行一次回调的时间主要花费在了进行数据转换和扫描匹配上.如果时间打印的再详细些的话就会发现时间主要用在扫描匹配部分.

当运行时间长了之后,会发现进行扫描匹配的时间明显变长了.

数据转换与扫描匹配用时: 0.0942895 秒。
执行一次回调用时: 0.094357 秒。
数据转换与扫描匹配用时: 0.0934194 秒。
执行一次回调用时: 0.093491 秒。
# 一段时间之后
数据转换与扫描匹配用时: 0.119987 秒。
执行一次回调用时: 0.120077 秒。
数据转换与扫描匹配用时: 0.13254 秒。
执行一次回调用时: 0.133221 秒。

雷达是10hz的,处理一次回调就要花0.12秒了,这时就会在运算过程中丢弃雷达数据.**这个原因猜测是由于地图变大之后,与地图的匹配更加耗时导致的.**具体是哪一部分导致的还有待推敲。

我在代码里将多分辨率地图的层数设置成了3层,如果设置成2层的话也会减小时间消耗.但是匹配的效果就会变差.

hector里程计的频率

如果一直通过rostopic hz 来查看 雷达与odom_hector的频率的话,就会发现,到后期 odom_hector的频率已经从 10hz 下降到 7.9hz 了.

   topic        rate    min_delta   max_delta   std_dev    window
=================================================================
/laser_scan    9.986    0.09054     0.1107      0.003959   20    
/map           0.5228   1.913       1.913       0.0        20    
/odom_hector   10.2     0.08047     0.1209      0.01026    2     
# 一段时间之后topic        rate    min_delta   max_delta   std_dev    window
=================================================================
/laser_scan    10.0     0.08059     0.1166      0.004054   1984  
/map           0.5004   1.783       2.174       0.06475    1984  
/odom_hector   7.979    0.08047     0.3229      0.02474    100   

4 总结与Next

本篇文章对Hecot的代码进行了简单的重构,发布了map与odom的tf,实现了一个基于scan_to_map的里程计,并构建出了第一张较成功的栅格地图.

下一篇文章将对 Hector 论文中的公式进行手动推倒,以及相应的代码讲解.

再下一篇文章将使用里程计数据进行SLAM,将基于 Karto 进行相关代码的实现.


文章将在 公众号: 从零开始搭SLAM 进行同步更新,欢迎大家关注,可以在公众号中添加我的微信,进激光SLAM交流群,大家一起交流SLAM技术。

同时,也希望您将这个公众号推荐给您身边做激光SLAM的人们,大家共同进步。

如果您对我写的文章有什么建议,或者想要看哪方面功能如何实现的,请直接在公众号中回复,我可以收到,并将认真考虑您的建议。

在这里插入图片描述

这篇关于从零开始搭二维激光SLAM --- 简单重写 Hector SLAM的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/871205

相关文章

无人叉车3d激光slam多房间建图定位异常处理方案-墙体画线地图切分方案

墙体画线地图切分方案 针对问题:墙体两侧特征混淆误匹配,导致建图和定位偏差,表现为过门跳变、外月台走歪等 ·解决思路:预期的根治方案IGICP需要较长时间完成上线,先使用切分地图的工程化方案,即墙体两侧切分为不同地图,在某一侧只使用该侧地图进行定位 方案思路 切分原理:切分地图基于关键帧位置,而非点云。 理论基础:光照是直线的,一帧点云必定只能照射到墙的一侧,无法同时照到两侧实践考虑:关

poj2576(二维背包)

题意:n个人分成两组,两组人数只差小于1 , 并且体重只差最小 对于人数要求恰好装满,对于体重要求尽量多,一开始没做出来,看了下解题,按照自己的感觉写,然后a了 状态转移方程:dp[i][j] = max(dp[i][j],dp[i-1][j-c[k]]+c[k]);其中i表示人数,j表示背包容量,k表示输入的体重的 代码如下: #include<iostream>#include<

hdu2159(二维背包)

这是我的第一道二维背包题,没想到自己一下子就A了,但是代码写的比较乱,下面的代码是我有重新修改的 状态转移:dp[i][j] = max(dp[i][j], dp[i-1][j-c[z]]+v[z]); 其中dp[i][j]表示,打了i个怪物,消耗j的耐力值,所得到的最大经验值 代码如下: #include<iostream>#include<algorithm>#include<

csu 1446 Problem J Modified LCS (扩展欧几里得算法的简单应用)

这是一道扩展欧几里得算法的简单应用题,这题是在湖南多校训练赛中队友ac的一道题,在比赛之后请教了队友,然后自己把它a掉 这也是自己独自做扩展欧几里得算法的题目 题意:把题意转变下就变成了:求d1*x - d2*y = f2 - f1的解,很明显用exgcd来解 下面介绍一下exgcd的一些知识点:求ax + by = c的解 一、首先求ax + by = gcd(a,b)的解 这个

hdu2289(简单二分)

虽说是简单二分,但是我还是wa死了  题意:已知圆台的体积,求高度 首先要知道圆台体积怎么求:设上下底的半径分别为r1,r2,高为h,V = PI*(r1*r1+r1*r2+r2*r2)*h/3 然后以h进行二分 代码如下: #include<iostream>#include<algorithm>#include<cstring>#include<stack>#includ

usaco 1.3 Prime Cryptarithm(简单哈希表暴搜剪枝)

思路: 1. 用一个 hash[ ] 数组存放输入的数字,令 hash[ tmp ]=1 。 2. 一个自定义函数 check( ) ,检查各位是否为输入的数字。 3. 暴搜。第一行数从 100到999,第二行数从 10到99。 4. 剪枝。 代码: /*ID: who jayLANG: C++TASK: crypt1*/#include<stdio.h>bool h

uva 10387 Billiard(简单几何)

题意是一个球从矩形的中点出发,告诉你小球与矩形两条边的碰撞次数与小球回到原点的时间,求小球出发时的角度和小球的速度。 简单的几何问题,小球每与竖边碰撞一次,向右扩展一个相同的矩形;每与横边碰撞一次,向上扩展一个相同的矩形。 可以发现,扩展矩形的路径和在当前矩形中的每一段路径相同,当小球回到出发点时,一条直线的路径刚好经过最后一个扩展矩形的中心点。 最后扩展的路径和横边竖边恰好组成一个直

poj 1113 凸包+简单几何计算

题意: 给N个平面上的点,现在要在离点外L米处建城墙,使得城墙把所有点都包含进去且城墙的长度最短。 解析: 韬哥出的某次训练赛上A出的第一道计算几何,算是大水题吧。 用convexhull算法把凸包求出来,然后加加减减就A了。 计算见下图: 好久没玩画图了啊好开心。 代码: #include <iostream>#include <cstdio>#inclu

uva 10130 简单背包

题意: 背包和 代码: #include <iostream>#include <cstdio>#include <cstdlib>#include <algorithm>#include <cstring>#include <cmath>#include <stack>#include <vector>#include <queue>#include <map>

HDU 2159 二维完全背包

FATE 最近xhd正在玩一款叫做FATE的游戏,为了得到极品装备,xhd在不停的杀怪做任务。久而久之xhd开始对杀怪产生的厌恶感,但又不得不通过杀怪来升完这最后一级。现在的问题是,xhd升掉最后一级还需n的经验值,xhd还留有m的忍耐度,每杀一个怪xhd会得到相应的经验,并减掉相应的忍耐度。当忍耐度降到0或者0以下时,xhd就不会玩这游戏。xhd还说了他最多只杀s只怪。请问他能