继续学习slam定位文章,本篇学习笔记来源于这篇文章。主要内容是对于之前设计的前端里程计实现部分做代码和结构的优化。
在这篇学习文章中,大佬主要讲述了一种实际中工程开发的思想,就是先将想法实现,然后在进行后续的优化。即先写简单的框架,把代码跑起来,然后在不断的增加功能和优化结构。这样一来开发的效率会提高很多。
然后就是具体针对front_end的优化过程。主要优化了这几个点:
1.功能模块降耦合,将voxel滤波器单独写成了一个模块,方便之后更换不同的滤波器算法,也将ndt配准单独写成了一个模块,方便之后更换不同的配准算法,比如icp,pl-icp等。这里采用了c++的三大特性之多态,就是利用虚函数的继承特点。具体请看文章,这篇文章说的很详细。采用多态后,之后对于前端的优化实现就更加方便了。这应该也是为什么耦合程度低比较受欢迎,听说cartograph的耦合程度就很高。
2.配置文件,采用yaml格式作为配置文件格式,是ros中的config文件里面的内容,第一次遇见这种具体的操作,之前在学习ros的时候只是在对于ros文件系统的简介里面看到了。首先要了解yaml是什么?这篇文章写的很详细。简单来说是一种文件格式,类似与xml、json一样。但相关的格式有不同的要求。具体应用这篇文章写的比较详细。(ps:发现一个bug,firefox居然不能打开简书?)。总结来说就是当工程文件有许多的参数在未来的工程使用的时候需要调整的时候,使用yaml格式比较方便调参。
3.保存点云文件的形式,之前的设计是关键帧点云和全局点云一直运行在内存中,但如果内存不大的话就容易崩溃,不利于大场景建图,这次优化后直接存储在硬盘中,最后在通过ros-service命令实现显示全局地图。关于ros-services,这篇文章写的很好。简单来说就是对比ros的topic通讯,services通讯会有一个反馈,来显示说是否订阅到发送的信息,而topic是没有这个反馈的。
4.ros流程封装,这里就是对于最终的执行文件进行优化,将主要的的实现功能分别封装在一个类中,这样node文件里面的实现就会很简便,这样的实现就十分简洁且容易调试了。
下面就对所有的文件、源码进行分析:
我比较喜欢看头文件分析源码,从最后生成的执行文件front_end_node.cpp开始。
#include
#include "glog/logging.h"
#include
#include "lidar_localization/global_defination/global_defination.h"
#include "lidar_localization/front_end/front_end_flow.hpp"
最后一行就是先调用的头文件,然后在看front_end_flow.hpp的头文件。
#include
#include "lidar_localization/subscriber/cloud_subscriber.hpp"
#include "lidar_localization/subscriber/imu_subscriber.hpp"
#include "lidar_localization/subscriber/gnss_subscriber.hpp"
#include "lidar_localization/tf_listener/tf_listener.hpp"
#include "lidar_localization/publisher/cloud_publisher.hpp"
#include "lidar_localization/publisher/odometry_publisher.hpp"
#include "lidar_localization/front_end/front_end.hpp"
最后一行是front_end.hpp,打开front_end.hpp。
#include
#include
#include
#include
#include
#include "lidar_localization/sensor_data/cloud_data.hpp"
#include "lidar_localization/models/registration/ndt_registration.hpp"
#include "lidar_localization/models/cloud_filter/voxel_filter.hpp"
到这里就可以确定有两个基础的hpp,分别是registration和cloud_filter,它们对应了优化第一步的功能模块降耦合。
这个模块共有三份文件:cloud_filter_interface.hpp、voxel_filter.hpp和voxel_filter.cpp。
从cloud_filter_interface.hpp开始:
#ifndef LIDAR_LOCALIZATION_MODELS_CLOUD_FILTER_CLOUD_FILTER_INTERFACE_HPP_
#define LIDAR_LOCALIZATION_MODELS_CLOUD_FILTER_CLOUD_FILTER_INTERFACE_HPP_
#include
#include "lidar_localization/sensor_data/cloud_data.hpp"
namespace lidar_localization {
class CloudFilterInterface {
public:
virtual ~CloudFilterInterface() = default;
virtual bool Filter(const CloudData::CLOUD_PTR& input_cloud_ptr, CloudData::CLOUD_PTR& filtered_cloud_ptr) = 0;
};
}
#endif
主要作用是定义了叫CloudFilterInterface的基类,其中有一个默认析构函数,一个Filter的纯虚函数。主要作用是提供点云过滤算法选择的函数接口。
voxel_filter.hpp:
#ifndef LIDAR_LOCALIZATION_MODELS_CLOUD_FILTER_VOXEL_FILTER_HPP_
#define LIDAR_LOCALIZATION_MODELS_CLOUD_FILTER_VOXEL_FILTER_HPP_
#include
#include "lidar_localization/models/cloud_filter/cloud_filter_interface.hpp"
namespace lidar_localization {
class VoxelFilter: public CloudFilterInterface {
public:
VoxelFilter(const YAML::Node& node);
VoxelFilter(float leaf_size_x, float leaf_size_y, float leaf_size_z);
bool Filter(const CloudData::CLOUD_PTR& input_cloud_ptr, CloudData::CLOUD_PTR& filtered_cloud_ptr) override;
private:
bool SetFilterParam(float leaf_size_x, float leaf_size_y, float leaf_size_z);
private:
pcl::VoxelGrid voxel_filter_;
};
}
#endif
定义了一个名为VoxelFilter的类,它继承于CloudFilterInterface。有两个构造函数,第一个是使用yaml文件来进行初始化,第二个是使用三个参数来初始化生成对象。然后就是一个公有的成员函数Filter。最后是一个私有的成员函数,初步猜测SetFilterParam是用来设置滤波器参数,如果设置了就返回true,否则返回false。
最后是voxel_filter.cpp:
#include "lidar_localization/models/cloud_filter/voxel_filter.hpp"
#include "glog/logging.h"
namespace lidar_localization {
VoxelFilter::VoxelFilter(const YAML::Node& node) {
float leaf_size_x = node["leaf_size"][0].as();
float leaf_size_y = node["leaf_size"][1].as();
float leaf_size_z = node["leaf_size"][2].as();
SetFilterParam(leaf_size_x, leaf_size_y, leaf_size_z);
}
VoxelFilter::VoxelFilter(float leaf_size_x, float leaf_size_y, float leaf_size_z) {
SetFilterParam(leaf_size_x, leaf_size_y, leaf_size_z);
}
bool VoxelFilter::SetFilterParam(float leaf_size_x, float leaf_size_y, float leaf_size_z) {
voxel_filter_.setLeafSize(leaf_size_x, leaf_size_y, leaf_size_z);
LOG(INFO) << "Voxel Filter 的参数为:" << std::endl
<< leaf_size_x << ", "
<< leaf_size_y << ", "
<< leaf_size_z
<< std::endl << std::endl;
return true;
}
bool VoxelFilter::Filter(const CloudData::CLOUD_PTR& input_cloud_ptr, CloudData::CLOUD_PTR& filtered_cloud_ptr) {
voxel_filter_.setInputCloud(input_cloud_ptr);
voxel_filter_.filter(*filtered_cloud_ptr);
return true;
}
}
这里的源码写的十分清晰,两个构造函数都获得的参数然后调用到SetFilterParam函数里面,而这个函数作用就是voxel_filter_的初始化文件。最后是Filter函数的定义,也是voxel_filter_的初始化定义,分别是是设置输入的点云,和经过voxel过滤后的点云。
这个模块共有三份文件:registration_interface.hpp、ndt_registration.hpp和ndt_registration.cpp。
从registration_interface.hpp开始看:
#ifndef LIDAR_LOCALIZATION_MODELS_REGISTRATION_INTERFACE_HPP_
#define LIDAR_LOCALIZATION_MODELS_REGISTRATION_INTERFACE_HPP_
#include
#include
#include "lidar_localization/sensor_data/cloud_data.hpp"
namespace lidar_localization {
class RegistrationInterface {
public:
virtual ~RegistrationInterface() = default;
virtual bool SetInputTarget(const CloudData::CLOUD_PTR& input_target) = 0;
virtual bool ScanMatch(const CloudData::CLOUD_PTR& input_source,
const Eigen::Matrix4f& predict_pose,
CloudData::CLOUD_PTR& result_cloud_ptr,
Eigen::Matrix4f& result_pose) = 0;
};
}
#endif
声明一个基类RegistrationInterface,有一个虚函数,是它的默认析构函数,两个纯虚函数,SetInputTarget和ScanMatch。
关于ndt_registration.hpp:
#ifndef LIDAR_LOCALIZATION_MODELS_REGISTRATION_NDT_REGISTRATION_HPP_
#define LIDAR_LOCALIZATION_MODELS_REGISTRATION_NDT_REGISTRATION_HPP_
#include
#include "lidar_localization/models/registration/registration_interface.hpp"
namespace lidar_localization {
class NDTRegistration: public RegistrationInterface {
public:
NDTRegistration(const YAML::Node& node);
NDTRegistration(float res, float step_size, float trans_eps, int max_iter);
bool SetInputTarget(const CloudData::CLOUD_PTR& input_target) override;
bool ScanMatch(const CloudData::CLOUD_PTR& input_source,
const Eigen::Matrix4f& predict_pose,
CloudData::CLOUD_PTR& result_cloud_ptr,
Eigen::Matrix4f& result_pose) override;
private:
bool SetRegistrationParam(float res, float step_size, float trans_eps, int max_iter);
private:
pcl::NormalDistributionsTransform::Ptr ndt_ptr_;
};
}
#endif
定义了一个NDTRegistration的基类,继承于RegistrationInterface类。定义了两个构造函数一个使用yaml获取参数,一个使用输入的参数。两个公有函数都来源于基类的纯虚函数。一个私有的设置参数的执行函数,一个就是ndt的成员变量。
最后是ndt_registration.cpp:
#include "lidar_localization/models/registration/ndt_registration.hpp"
#include "glog/logging.h"
namespace lidar_localization {
NDTRegistration::NDTRegistration(const YAML::Node& node)
:ndt_ptr_(new pcl::NormalDistributionsTransform()) {
float res = node["res"].as();
float step_size = node["step_size"].as();
float trans_eps = node["trans_eps"].as();
int max_iter = node["max_iter"].as();
SetRegistrationParam(res, step_size, trans_eps, max_iter);
}
NDTRegistration::NDTRegistration(float res, float step_size, float trans_eps, int max_iter)
:ndt_ptr_(new pcl::NormalDistributionsTransform()) {
SetRegistrationParam(res, step_size, trans_eps, max_iter);
}
bool NDTRegistration::SetRegistrationParam(float res, float step_size, float trans_eps, int max_iter) {
ndt_ptr_->setResolution(res);
ndt_ptr_->setStepSize(step_size);
ndt_ptr_->setTransformationEpsilon(trans_eps);
ndt_ptr_->setMaximumIterations(max_iter);
LOG(INFO) << "NDT 的匹配参数为:" << std::endl
<< "res: " << res << ", "
<< "step_size: " << step_size << ", "
<< "trans_eps: " << trans_eps << ", "
<< "max_iter: " << max_iter
<< std::endl << std::endl;
return true;
}
bool NDTRegistration::SetInputTarget(const CloudData::CLOUD_PTR& input_target) {
ndt_ptr_->setInputTarget(input_target);
return true;
}
bool NDTRegistration::ScanMatch(const CloudData::CLOUD_PTR& input_source,
const Eigen::Matrix4f& predict_pose,
CloudData::CLOUD_PTR& result_cloud_ptr,
Eigen::Matrix4f& result_pose) {
ndt_ptr_->setInputSource(input_source);
ndt_ptr_->align(*result_cloud_ptr, predict_pose);
result_pose = ndt_ptr_->getFinalTransformation();
return true;
}
}
一个使用yaml初始化的构造函数,里面有SetRegistrationParam的执行函数,另一个是使用参数初始化的构造函数。然后就是SetRegistrationParam函数的定义,就是对于ndt_ptr_的设置分辨率、步长最大迭代次数等。SetInputTarget是设置目标点云,ScanMatch是设置源点云和根据ndt配准后的点云以及result_pose的变换矩阵。
主要有front_end和front_end_flow两个文件,首先分析front_end.hpp:
#ifndef LIDAR_LOCALIZATION_FRONT_END_FRONT_END_HPP_
#define LIDAR_LOCALIZATION_FRONT_END_FRONT_END_HPP_
#include
#include
#include
#include
#include
#include "lidar_localization/sensor_data/cloud_data.hpp"
#include "lidar_localization/models/registration/ndt_registration.hpp"
#include "lidar_localization/models/cloud_filter/voxel_filter.hpp"
namespace lidar_localization {
class FrontEnd {
public:
struct Frame {
Eigen::Matrix4f pose = Eigen::Matrix4f::Identity();
CloudData cloud_data;
};
public:
FrontEnd();
bool InitWithConfig();
bool Update(const CloudData& cloud_data, Eigen::Matrix4f& cloud_pose);
bool SetInitPose(const Eigen::Matrix4f& init_pose);
bool SaveMap();
bool GetNewLocalMap(CloudData::CLOUD_PTR& local_map_ptr);
bool GetNewGlobalMap(CloudData::CLOUD_PTR& global_map_ptr);
bool GetCurrentScan(CloudData::CLOUD_PTR& current_scan_ptr);
private:
bool InitParam(const YAML::Node& config_node);
bool InitDataPath(const YAML::Node& config_node);
bool InitRegistration(std::shared_ptr& registration_ptr, const YAML::Node& config_node);
bool InitFilter(std::string filter_user, std::shared_ptr& filter_ptr, const YAML::Node& config_node);
bool UpdateWithNewFrame(const Frame& new_key_frame);
private:
std::string data_path_ = "";
std::shared_ptr frame_filter_ptr_;
std::shared_ptr local_map_filter_ptr_;
std::shared_ptr display_filter_ptr_;
std::shared_ptr registration_ptr_;
std::deque local_map_frames_;
std::deque global_map_frames_;
bool has_new_local_map_ = false;
bool has_new_global_map_ = false;
CloudData::CLOUD_PTR local_map_ptr_;
CloudData::CLOUD_PTR global_map_ptr_;
CloudData::CLOUD_PTR result_cloud_ptr_;
Frame current_frame_;
Eigen::Matrix4f init_pose_ = Eigen::Matrix4f::Identity();
float key_frame_distance_ = 2.0;
int local_frame_num_ = 20;
};
}
#endif
有一个Frame的数据结构里面是一个4×4的变换矩阵和一个CloudData结构的点云对象。和最初版本的一样。然后就是FrontEnd()的默认构造函数,一个根据config里面yaml文件初始化的函数InitWithConfig,一个Updata更新点云的函数,一个设置最初位姿的函数SetInitPose,然后就是保存地图和获取局部点云地图、获取全局点云地图和获取当前帧点云地图。然后就是私有的成员函数,InitParam函数,主要功能是读取yaml文件里面关于key_frame_distance和local_frame_num的参数并设置在类的私有变量上。InitDataPath函数,也是读取yaml文件的内容,主要功能是设置点云地图的存储位置。InitRegistration函数主要作用是读取yaml文件的内容,然后设置了配准方法的指针,主要内容就是输出所选取的配准方法并初始化第一个参数。InitFilter函数作用和前面函数的作用类似,也是读取yaml文件内容并选择合适的点云过滤器,初始化。然后就是和最初前端里程计类似函数UpdataWithNewFrame里面更新了将关键帧点云存储到硬盘里面的功能 。然后就是私有的成员变量,三个滤波器的指针,一个配准方法的指针,局部点云数据的队列,全局点云数据的队列。两个标志位,三个点云指针,一个Frame对象,一个初始化的4×4的矩阵,最后是选取关键帧距离和局部地图数量的变量。
front_end.cpp:
#include "lidar_localization/front_end/front_end.hpp"
#include
#include
#include
#include
#include "glog/logging.h"
#include "lidar_localization/global_defination/global_defination.h"
namespace lidar_localization {
FrontEnd::FrontEnd()
:local_map_ptr_(new CloudData::CLOUD()),
global_map_ptr_(new CloudData::CLOUD()),
result_cloud_ptr_(new CloudData::CLOUD()) {
InitWithConfig();
}
bool FrontEnd::InitWithConfig() {
std::string config_file_path = WORK_SPACE_PATH + "/config/front_end/config.yaml";
YAML::Node config_node = YAML::LoadFile(config_file_path);
InitDataPath(config_node);
InitRegistration(registration_ptr_, config_node);
InitFilter("local_map", local_map_filter_ptr_, config_node);
InitFilter("frame", frame_filter_ptr_, config_node);
InitFilter("display", display_filter_ptr_, config_node);
return true;
}
bool FrontEnd::InitParam(const YAML::Node& config_node) {
key_frame_distance_ = config_node["key_frame_distance"].as();
local_frame_num_ = config_node["local_frame_num"].as();
return true;
}
bool FrontEnd::InitDataPath(const YAML::Node& config_node) {
data_path_ = config_node["data_path"].as();
if (data_path_ == "./") {
data_path_ = WORK_SPACE_PATH;
}
data_path_ += "/slam_data";
if (boost::filesystem::is_directory(data_path_)) {
boost::filesystem::remove_all(data_path_);
}
boost::filesystem::create_directory(data_path_);
if (!boost::filesystem::is_directory(data_path_)) {
LOG(WARNING) << "文件夹 " << data_path_ << " 未创建成功!";
return false;
} else {
LOG(INFO) << "地图点云存放地址:" << data_path_;
}
std::string key_frame_path = data_path_ + "/key_frames";
boost::filesystem::create_directory(data_path_ + "/key_frames");
if (!boost::filesystem::is_directory(key_frame_path)) {
LOG(WARNING) << "文件夹 " << key_frame_path << " 未创建成功!";
return false;
} else {
LOG(INFO) << "关键帧点云存放地址:" << key_frame_path << std::endl << std::endl;
}
return true;
}
bool FrontEnd::InitRegistration(std::shared_ptr& registration_ptr, const YAML::Node& config_node) {
std::string registration_method = config_node["registration_method"].as();
LOG(INFO) << "点云匹配方式为:" << registration_method;
if (registration_method == "NDT") {
registration_ptr = std::make_shared(config_node[registration_method]);
} else {
LOG(ERROR) << "没找到与 " << registration_method << " 相对应的点云匹配方式!";
return false;
}
return true;
}
bool FrontEnd::InitFilter(std::string filter_user, std::shared_ptr& filter_ptr, const YAML::Node& config_node) {
std::string filter_mothod = config_node[filter_user + "_filter"].as();
LOG(INFO) << filter_user << "选择的滤波方法为:" << filter_mothod;
if (filter_mothod == "voxel_filter") {
filter_ptr = std::make_shared(config_node[filter_mothod][filter_user]);
} else {
LOG(ERROR) << "没有为 " << filter_user << " 找到与 " << filter_mothod << " 相对应的滤波方法!";
return false;
}
return true;
}
bool FrontEnd::Update(const CloudData& cloud_data, Eigen::Matrix4f& cloud_pose) {
current_frame_.cloud_data.time = cloud_data.time;
std::vector indices;
pcl::removeNaNFromPointCloud(*cloud_data.cloud_ptr, *current_frame_.cloud_data.cloud_ptr, indices);
CloudData::CLOUD_PTR filtered_cloud_ptr(new CloudData::CLOUD());
frame_filter_ptr_->Filter(current_frame_.cloud_data.cloud_ptr, filtered_cloud_ptr);
static Eigen::Matrix4f step_pose = Eigen::Matrix4f::Identity();
static Eigen::Matrix4f last_pose = init_pose_;
static Eigen::Matrix4f predict_pose = init_pose_;
static Eigen::Matrix4f last_key_frame_pose = init_pose_;
// 局部地图容器中没有关键帧,代表是第一帧数据
// 此时把当前帧数据作为第一个关键帧,并更新局部地图容器和全局地图容器
if (local_map_frames_.size() == 0) {
current_frame_.pose = init_pose_;
UpdateWithNewFrame(current_frame_);
cloud_pose = current_frame_.pose;
return true;
}
// 不是第一帧,就正常匹配
registration_ptr_->ScanMatch(filtered_cloud_ptr, predict_pose, result_cloud_ptr_, current_frame_.pose);
cloud_pose = current_frame_.pose;
// 更新相邻两帧的相对运动
step_pose = last_pose.inverse() * current_frame_.pose;
predict_pose = current_frame_.pose * step_pose;
last_pose = current_frame_.pose;
// 匹配之后根据距离判断是否需要生成新的关键帧,如果需要,则做相应更新
if (fabs(last_key_frame_pose(0,3) - current_frame_.pose(0,3)) +
fabs(last_key_frame_pose(1,3) - current_frame_.pose(1,3)) +
fabs(last_key_frame_pose(2,3) - current_frame_.pose(2,3)) > key_frame_distance_) {
UpdateWithNewFrame(current_frame_);
last_key_frame_pose = current_frame_.pose;
}
return true;
}
bool FrontEnd::SetInitPose(const Eigen::Matrix4f& init_pose) {
init_pose_ = init_pose;
return true;
}
bool FrontEnd::UpdateWithNewFrame(const Frame& new_key_frame) {
// 把关键帧点云存储到硬盘里,节省内存
std::string file_path = data_path_ + "/key_frames/key_frame_" + std::to_string(global_map_frames_.size()) + ".pcd";
pcl::io::savePCDFileBinary(file_path, *new_key_frame.cloud_data.cloud_ptr);
Frame key_frame = new_key_frame;
// 这一步的目的是为了把关键帧的点云保存下来
// 由于用的是共享指针,所以直接复制只是复制了一个指针而已
// 此时无论你放多少个关键帧在容器里,这些关键帧点云指针都是指向的同一个点云
key_frame.cloud_data.cloud_ptr.reset(new CloudData::CLOUD(*new_key_frame.cloud_data.cloud_ptr));
CloudData::CLOUD_PTR transformed_cloud_ptr(new CloudData::CLOUD());
// 更新局部地图
local_map_frames_.push_back(key_frame);
while (local_map_frames_.size() > static_cast(local_frame_num_)) {
local_map_frames_.pop_front();
}
local_map_ptr_.reset(new CloudData::CLOUD());
for (size_t i = 0; i < local_map_frames_.size(); ++i) {
pcl::transformPointCloud(*local_map_frames_.at(i).cloud_data.cloud_ptr,
*transformed_cloud_ptr,
local_map_frames_.at(i).pose);
*local_map_ptr_ += *transformed_cloud_ptr;
}
has_new_local_map_ = true;
// 更新ndt匹配的目标点云
// 关键帧数量还比较少的时候不滤波,因为点云本来就不多,太稀疏影响匹配效果
if (local_map_frames_.size() < 10) {
registration_ptr_->SetInputTarget(local_map_ptr_);
} else {
CloudData::CLOUD_PTR filtered_local_map_ptr(new CloudData::CLOUD());
local_map_filter_ptr_->Filter(local_map_ptr_, filtered_local_map_ptr);
registration_ptr_-> (filtered_local_map_ptr);
}
// 保存所有关键帧信息在容器里
// 存储之前,点云要先释放,因为已经存到了硬盘里,不释放也达不到节省内存的目的
key_frame.cloud_data.cloud_ptr.reset(new CloudData::CLOUD());
global_map_frames_.push_back(key_frame);
return true;
}
bool FrontEnd::SaveMap() {
global_map_ptr_.reset(new CloudData::CLOUD());
std::string key_frame_path = "";
CloudData::CLOUD_PTR key_frame_cloud_ptr(new CloudData::CLOUD());
CloudData::CLOUD_PTR transformed_cloud_ptr(new CloudData::CLOUD());
for (size_t i = 0; i < global_map_frames_.size(); ++i) {
key_frame_path = data_path_ + "/key_frames/key_frame_" + std::to_string(i) + ".pcd";
pcl::io::loadPCDFile(key_frame_path, *key_frame_cloud_ptr);
pcl::transformPointCloud(*key_frame_cloud_ptr,
*transformed_cloud_ptr,
global_map_frames_.at(i).pose);
*global_map_ptr_ += *transformed_cloud_ptr;
}
std::string map_file_path = data_path_ + "/map.pcd";
pcl::io::savePCDFileBinary(map_file_path, *global_map_ptr_);
has_new_global_map_ = true;
return true;
}
bool FrontEnd::GetNewLocalMap(CloudData::CLOUD_PTR& local_map_ptr) {
if (has_new_local_map_) {
display_filter_ptr_->Filter(local_map_ptr_, local_map_ptr);
return true;
}
return false;
}
bool FrontEnd::GetNewGlobalMap(CloudData::CLOUD_PTR& global_map_ptr) {
if (has_new_global_map_) {
has_new_global_map_ = false;
display_filter_ptr_->Filter(global_map_ptr_, global_map_ptr);
global_map_ptr_.reset(new CloudData::CLOUD());
return true;
}
return false;
}
bool FrontEnd::GetCurrentScan(CloudData::CLOUD_PTR& current_scan_ptr) {
display_filter_ptr_->Filter(result_cloud_ptr_, current_scan_ptr);
return true;
}
}
首先是构造函数FrontEnd,它将三个点云地图初始化,然后调用了InitWithConfig函数进行其他私有成员变量初始化。接下来分析InitWithConfig函数,首先确定了config的文件的位置,然后利用YAML::LoadFile函数提取出来,赋值给config_node,它的数据类型是YAML::Node。然后就是调用了5个初始化函数,InitDataPath、InitRegistration、InitFilter(三个不同的user代表了三个不同的展示效果),分别是局部点云地图、关键帧和最后的显示。这里就体现了优化的特点,使用了一个模块然后通过不同输入参数,来生成不同的对象。顺着函数分析,InitDataPath函数,功能是读取yaml里面设置的data_path参数,然后生成data_path_文件的位置和关键帧存放的点云位置。InitRegisration函数,是读取yaml里面关于点云配准方法,并初始化第一个参数。InitFilter函数作用类似,判断是否选取的是不是voxel滤波器。然后是InitParam函数,就是读取yaml中key_frame_distance和local_frame_num的数据,并将它们初始化。最后是核心函数Updata,作用和之前写的前端类似,变化的是把一些功能模块封装了。比如说非第一帧的ScanMatch函数,作用还是利用ndt配准方法,第一个参数是经过voxel过滤的点云,第二个参数是初始化的位姿矩阵,第三个是经过ndt配准后的点云,第四个参数是结果ndt配准后的pose,其他的函数都类似。还有一个UpdataWithNewFrame函数,和之前的区别不大,只有关于点云存储方式做出了改变。
front_end_flow.hpp:
#ifndef LIDAR_LOCALIZATION_FRONT_END_FRONT_END_FLOW_HPP_
#define LIDAR_LOCALIZATION_FRONT_END_FRONT_END_FLOW_HPP_
#include
#include "lidar_localization/subscriber/cloud_subscriber.hpp"
#include "lidar_localization/subscriber/imu_subscriber.hpp"
#include "lidar_localization/subscriber/gnss_subscriber.hpp"
#include "lidar_localization/tf_listener/tf_listener.hpp"
#include "lidar_localization/publisher/cloud_publisher.hpp"
#include "lidar_localization/publisher/odometry_publisher.hpp"
#include "lidar_localization/front_end/front_end.hpp"
namespace lidar_localization {
class FrontEndFlow {
public:
FrontEndFlow(ros::NodeHandle& nh);
bool Run();
bool SaveMap();
bool PublishGlobalMap();
private:
bool ReadData();
bool InitCalibration();
bool InitGNSS();
bool HasData();
bool ValidData();
bool UpdateGNSSOdometry();
bool UpdateLaserOdometry();
bool PublishData();
private:
std::shared_ptr cloud_sub_ptr_;
std::shared_ptr imu_sub_ptr_;
std::shared_ptr gnss_sub_ptr_;
std::shared_ptr lidar_to_imu_ptr_;
std::shared_ptr cloud_pub_ptr_;
std::shared_ptr local_map_pub_ptr_;
std::shared_ptr global_map_pub_ptr_;
std::shared_ptr laser_odom_pub_ptr_;
std::shared_ptr gnss_pub_ptr_;
std::shared_ptr front_end_ptr_;
std::deque cloud_data_buff_;
std::deque imu_data_buff_;
std::deque gnss_data_buff_;
Eigen::Matrix4f lidar_to_imu_ = Eigen::Matrix4f::Identity();
CloudData current_cloud_data_;
IMUData current_imu_data_;
GNSSData current_gnss_data_;
CloudData::CLOUD_PTR local_map_ptr_;
CloudData::CLOUD_PTR global_map_ptr_;
CloudData::CLOUD_PTR current_scan_ptr_;
Eigen::Matrix4f gnss_odometry_ = Eigen::Matrix4f::Identity();
Eigen::Matrix4f laser_odometry_ = Eigen::Matrix4f::Identity();
};
}
#endif
声明了一个FrontEndFlow的类,一个参数是ros句柄的构造函数,三个执行函数Run负责运行整个程序、SaveMap负责在硬盘中保存地图、PublishGlobalMap负责将全局地图发布出来。私有程序:ReadData函数,作用是处理接受到的cloud_data、imu_data、gnss_data。InitCalbration函数作用是获得lidar和imu之间的位置关系。InitGNSS函数作用是初始化gnss_data的第一个数据。HasData函数判断获得是否有三个订阅的数据。ValidData函数针对三个数据分别取第一个数据进行判断。UpdataGNSSOdometry函数利用gnss数据和imu数据获得transform矩阵并转换了坐标系从imu转化到了lidar坐标系。UpdataLaserOdometry函数主要作用就是计算ndt配准方法求得的pose矩阵,然后计算它的odometry。PublishData函数就是发布gnss_odometry、current_scan和local_map数据。然后就是私有的成员变量,基本上是之前node文件里面的数据。
最后是front_end_flow.cpp:
#include "lidar_localization/front_end/front_end_flow.hpp"
#include "glog/logging.h"
namespace lidar_localization {
FrontEndFlow::FrontEndFlow(ros::NodeHandle& nh) {
cloud_sub_ptr_ = std::make_shared(nh, "/kitti/velo/pointcloud", 100000);
imu_sub_ptr_ = std::make_shared(nh, "/kitti/oxts/imu", 1000000);
gnss_sub_ptr_ = std::make_shared(nh, "/kitti/oxts/gps/fix", 1000000);
lidar_to_imu_ptr_ = std::make_shared(nh, "velo_link", "imu_link");
cloud_pub_ptr_ = std::make_shared(nh, "current_scan", 100, "/map");
local_map_pub_ptr_ = std::make_shared(nh, "local_map", 100, "/map");
global_map_pub_ptr_ = std::make_shared(nh, "global_map", 100, "/map");
laser_odom_pub_ptr_ = std::make_shared(nh, "laser_odom", "map", "lidar", 100);
gnss_pub_ptr_ = std::make_shared(nh, "gnss", "map", "lidar", 100);
front_end_ptr_ = std::make_shared();
local_map_ptr_.reset(new CloudData::CLOUD());
global_map_ptr_.reset(new CloudData::CLOUD());
current_scan_ptr_.reset(new CloudData::CLOUD());
}
bool FrontEndFlow::Run() {
ReadData();
if (!InitCalibration())
return false;
if (!InitGNSS())
return false;
while(HasData()) {
if (!ValidData())
continue;
UpdateGNSSOdometry();
if (UpdateLaserOdometry())
PublishData();
}
return true;
}
bool FrontEndFlow::ReadData() {
cloud_sub_ptr_->ParseData(cloud_data_buff_);
imu_sub_ptr_->ParseData(imu_data_buff_);
gnss_sub_ptr_->ParseData(gnss_data_buff_);
return true;
}
bool FrontEndFlow::InitCalibration() {
static bool calibration_received = false;
if (!calibration_received) {
if (lidar_to_imu_ptr_->LookupData(lidar_to_imu_)) {
calibration_received = true;
}
}
return calibration_received;
}
bool FrontEndFlow::InitGNSS() {
static bool gnss_inited = false;
if (!gnss_inited && gnss_data_buff_.size() > 0) {
GNSSData gnss_data = gnss_data_buff_.front();
gnss_data.InitOriginPosition();
gnss_inited = true;
}
return gnss_inited;
}
bool FrontEndFlow::HasData() {
if (cloud_data_buff_.size() == 0)
return false;
if (imu_data_buff_.size() == 0)
return false;
if (gnss_data_buff_.size() == 0)
return false;
return true;
}
bool FrontEndFlow::ValidData() {
current_cloud_data_ = cloud_data_buff_.front();
current_imu_data_ = imu_data_buff_.front();
current_gnss_data_ = gnss_data_buff_.front();
double d_time = current_cloud_data_.time - current_imu_data_.time;
if (d_time < -0.05) {
cloud_data_buff_.pop_front();
return false;
}
if (d_time > 0.05) {
imu_data_buff_.pop_front();
gnss_data_buff_.pop_front();
return false;
}
cloud_data_buff_.pop_front();
imu_data_buff_.pop_front();
gnss_data_buff_.pop_front();
return true;
}
bool FrontEndFlow::UpdateGNSSOdometry() {
gnss_odometry_ = Eigen::Matrix4f::Identity();
current_gnss_data_.UpdateXYZ();
gnss_odometry_(0,3) = current_gnss_data_.local_E;
gnss_odometry_(1,3) = current_gnss_data_.local_N;
gnss_odometry_(2,3) = current_gnss_data_.local_U;
gnss_odometry_.block<3,3>(0,0) = current_imu_data_.GetOrientationMatrix();
gnss_odometry_ *= lidar_to_imu_;
return true;
}
bool FrontEndFlow::UpdateLaserOdometry() {
static bool front_end_pose_inited = false;
if (!front_end_pose_inited) {
front_end_pose_inited = true;
front_end_ptr_->SetInitPose(gnss_odometry_);
laser_odometry_ = gnss_odometry_;
return true;
}
laser_odometry_ = Eigen::Matrix4f::Identity();
if (front_end_ptr_->Update(current_cloud_data_, laser_odometry_))
return true;
else
return false;
}
bool FrontEndFlow::PublishData() {
gnss_pub_ptr_->Publish(gnss_odometry_);
laser_odom_pub_ptr_->Publish(laser_odometry_);
front_end_ptr_->GetCurrentScan(current_scan_ptr_);
cloud_pub_ptr_->Publish(current_scan_ptr_);
if (front_end_ptr_->GetNewLocalMap(local_map_ptr_))
local_map_pub_ptr_->Publish(local_map_ptr_);
return true;
}
bool FrontEndFlow::SaveMap() {
return front_end_ptr_->SaveMap();
}
bool FrontEndFlow::PublishGlobalMap() {
if (front_end_ptr_->GetNewGlobalMap(global_map_ptr_)) {
global_map_pub_ptr_->Publish(global_map_ptr_);
global_map_ptr_.reset(new CloudData::CLOUD());
}
return true;
}
}
构造函数就将所有的私有变量初始化了,以智能指针的方式。然后开始按顺序分析函数,Run函数就是整个的执行函数,它首先调用ReadData函数,来读取数据,之后若不能初始化外参标定、不能初始化GNSS就直接报错,但获得数据之后就先更新GNSS的odometry,获得LaserOdometry的数据后在发布所有的odometry。
仔细看来就是将之前的front_end部分拆分开了,很多功能都独立写了模块类。
这里使用了service方法来判断是否有保存地图的指令,如果有,就会调用保存地图模块和发布全局地图函数,最后返回true。详细的关于ros-service的说明在前面提及了。
tag5就像大佬在文章中写的那样,是对于工程代码结构的优化,
降低了代码之间的耦合性;
引入了yaml的参数设置格式,方便未来调用不同的滤波器和配准方法;
将点云文件的生成放在了硬盘中,并使用了ros-service方法来决定是否显示全局地图。
总体来说,代码功能性的变化不大,结构变化比较大。但对于我而言阅读的难度却比之前的源码上升了好多,应该还是自己不熟悉相关的工程应用实施过程的原因,代码水平也不行,还是要多刷leetcode、多看别人优秀的源码,才能提升自己的能力。
练习:
准备了3个练习,第一个是针对yaml格式的练习,第二个是针对ros-srevice的练习,第三个是针对ndt配准的练习,希望本周可以抽出时间完成。希望本月可以把大佬关于定位的系列文章学习完毕。