结合书籍《自动驾驶与机器人中的SLAM技术:从理论到实践》和配套源码,来理解松耦合LIO。
松耦合LIO系统的代码实现分为3个部分:
1、将IMU和Lidar数据同步。
2、Lidar运动补偿。
3、里程计配准。
数据准备
点云数据处理
统一点云接口,点云数据统一转换为PCL格式,支持多种类型的点云。
src/ch7/loosely_coupled_lio/cloud_convert.h
/**
* 预处理雷达点云
*
* 将Velodyne, ouster, avia等数据转到FullCloud
* 该类由MessageSync类持有,负责将收到的雷达消息与IMU同步并预处理后,再交给LO/LIO算法
*/
class CloudConvert {
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW
enum class LidarType {
AVIA = 1, // 大疆的固态雷达
VELO32, // Velodyne 32线
OUST64, // ouster 64线
};
CloudConvert() = default;
~CloudConvert() = default;
/**
* 处理livox avia 点云
* @param msg
* @param pcl_out
*/
void Process(const livox_ros_driver::CustomMsg::ConstPtr &msg, FullCloudPtr &pcl_out);
/**
* 处理sensor_msgs::PointCloud2点云
* @param msg
* @param pcl_out
*/
void Process(const sensor_msgs::PointCloud2::ConstPtr &msg, FullCloudPtr &pcl_out);
/// 从YAML中读取参数
void LoadFromYAML(const std::string &yaml);
private:
void AviaHandler(const livox_ros_driver::CustomMsg::ConstPtr &msg);
void Oust64Handler(const sensor_msgs::PointCloud2::ConstPtr &msg);
void VelodyneHandler(const sensor_msgs::PointCloud2::ConstPtr &msg);
FullPointCloudType cloud_full_, cloud_out_; // 输出点云
LidarType lidar_type_ = LidarType::AVIA; // 雷达类型
int point_filter_num_ = 1; // 跳点
int num_scans_ = 6; // 扫描线数
float time_scale_ = 1e-3; // 雷达点的时间字段与秒的比例
};
IMU数据与Lidar数据同步
定义MessageSync类
src/ch7/loosely_coupled_lio/measure_sync.h
// IMU 数据与雷达同步
struct MeasureGroup {
MeasureGroup() { this->lidar_.reset(new FullPointCloudType()); };
double lidar_begin_time_ = 0; // 雷达包的起始时间
double lidar_end_time_ = 0; // 雷达的终止时间
FullCloudPtr lidar_ = nullptr; // 雷达点云
std::deque<IMUPtr> imu_; // 上一时时刻到现在的IMU读数
};
/**
* 将激光数据和IMU数据同步
*/
class MessageSync {
public:
using Callback = std::function<void(const MeasureGroup &)>;
MessageSync(Callback cb) : callback_(cb), conv_(new CloudConvert) {}
/// 初始化
void Init(const std::string &yaml);
/// 处理IMU数据
void ProcessIMU(IMUPtr imu) { }
//处理sensor_msgs::PointCloud2点云
void ProcessCloud(const sensor_msgs::PointCloud2::ConstPtr &msg) {}
/// 处理Livox点云
void ProcessCloud(const livox_ros_driver::CustomMsg::ConstPtr &msg) { }
};
bool MessageSync::Sync() {
if (lidar_buffer_.empty() || imu_buffer_.empty()) {
return false;
}
if (!lidar_pushed_) {
measures_.lidar_ = lidar_buffer_.front();
measures_.lidar_begin_time_ = time_buffer_.front();
lidar_end_time_ = measures_.lidar_begin_time_ + measures_.lidar_->points.back().time / double(1000);
measures_.lidar_end_time_ = lidar_end_time_;
lidar_pushed_ = true;
}
if (last_timestamp_imu_ < lidar_end_time_) {
return false;
}
double imu_time = imu_buffer_.front()->timestamp_;
measures_.imu_.clear();
while ((!imu_buffer_.empty()) && (imu_time < lidar_end_time_)) {
imu_time = imu_buffer_.front()->timestamp_;
if (imu_time > lidar_end_time_) {
break;
}
measures_.imu_.push_back(imu_buffer_.front());
imu_buffer_.pop_front();
}
lidar_buffer_.pop_front();
time_buffer_.pop_front();
lidar_pushed_ = false;
if (callback_) {
callback_(measures_);
}
return true;
}
主要流程
松耦合LIO系统,有一个ESKF对象,一个MessageSync对象,处理同步后的IMU和点云数据。
src/ch7/loosely_coupled_lio/loosely_lio.h
class LooselyLIO {
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW;
struct Options {
Options() {}
bool save_motion_undistortion_pcd_ = false; // 是否保存去畸变前后的点云
bool with_ui_ = true; // 是否带着UI
};
LooselyLIO(Options options);
~LooselyLIO() = default;
/// 从配置文件初始化
bool Init(const std::string &config_yaml);
/// 点云回调函数
void PCLCallBack(const sensor_msgs::PointCloud2::ConstPtr &msg);
void LivoxPCLCallBack(const livox_ros_driver::CustomMsg::ConstPtr &msg);
/// IMU回调函数
void IMUCallBack(IMUPtr msg_in);
/// 结束程序,退出UI
void Finish();
private:
/// 处理同步之后的IMU和雷达数据
void ProcessMeasurements(const MeasureGroup &meas);
bool LoadFromYAML(const std::string &yaml);
/// 尝试让IMU初始化
void TryInitIMU();
/// 利用IMU预测状态信息
/// 这段时间的预测数据会放入imu_states_里
void Predict();
/// 对measures_中的点云去畸变
void Undistort();
/// 执行一次配准和观测
void Align();
private:
/// modules
std::shared_ptr<MessageSync> sync_ = nullptr; // 消息同步器
StaticIMUInit imu_init_; // IMU静止初始化
std::shared_ptr<sad::IncrementalNDTLO> inc_ndt_lo_ = nullptr;
/// point clouds data
FullCloudPtr scan_undistort_{new FullPointCloudType()}; // scan after undistortion
SE3 pose_of_lo_;
Options options_;
// flags
bool imu_need_init_ = true; // 是否需要估计IMU初始零偏
bool flg_first_scan_ = true; // 是否第一个雷达
int frame_num_ = 0; // 帧数计数
// EKF data
MeasureGroup measures_; // 同步之后的IMU和点云
std::vector<NavStated> imu_states_; // ESKF预测期间的状态
ESKFD eskf_; // ESKF
SE3 TIL_; // Lidar与IMU之间外参
std::shared_ptr<ui::PangolinWindow> ui_ = nullptr;
};
主要处理逻辑
当IMU未初始化时, 使用静止初始化来估计IMU零偏。IMU初始化完毕后,使用IMU数据进行预测; 用预测数据对点云去畸变;对去畸变的点云做配准。
ProcessMeasurements()
void LooselyLIO::ProcessMeasurements(const MeasureGroup &meas) {
LOG(INFO) << "call meas, imu: " << meas.imu_.size() << ", lidar pts: " << meas.lidar_->size();
measures_ = meas;
if (imu_need_init_) {
// 初始化IMU系统
TryInitIMU();
return;
}
// 利用IMU数据进行状态预测
Predict();
// 对点云去畸变
Undistort();
// 配准
Align();
}
TryInitIMU()
void LooselyLIO::TryInitIMU() {
for (auto imu : measures_.imu_) {
imu_init_.AddIMU(*imu);
}
if (imu_init_.InitSuccess()) {
// 读取初始零偏,设置ESKF
sad::ESKFD::Options options;
// 噪声由初始化器估计
options.gyro_var_ = sqrt(imu_init_.GetCovGyro()[0]);
options.acce_var_ = sqrt(imu_init_.GetCovAcce()[0]);
eskf_.SetInitialConditions(options, imu_init_.GetInitBg(), imu_init_.GetInitBa(), imu_init_.GetGravity());
imu_need_init_ = false;
LOG(INFO) << "IMU初始化成功";
}
}
核心部分
预测
将IMU数据传递给滤波器,然后记录滤波器的名义状态变量。
src/ch7/loosely_coupled_lio/loosely_lio.cc
Predict()
void LooselyLIO::Predict() {
imu_states_.clear();
imu_states_.emplace_back(eskf_.GetNominalState());
/// 对IMU状态进行预测
for (auto &imu : measures_.imu_) {
eskf_.Predict(*imu);
imu_states_.emplace_back(eskf_.GetNominalState());
}
}
点云去畸变
使用IMU预测位姿进行运动补偿
运动补偿的转换公式:
IMU与雷达之间的外参
t时刻的IMU位姿
结束时刻的IMU位姿
单个扫描点的位姿(雷达坐标系下)
Undistort()
void LooselyLIO::Undistort() {
auto cloud = measures_.lidar_;
auto imu_state = eskf_.GetNominalState(); // 最后时刻的状态
SE3 T_end = SE3(imu_state.R_, imu_state.p_);
if (options_.save_motion_undistortion_pcd_) {
sad::SaveCloudToFile("./data/ch7/before_undist.pcd", *cloud);
}
/// 将所有点转到最后时刻状态上
std::for_each(std::execution::par_unseq, cloud->points.begin(), cloud->points.end(), [&](auto &pt) {
SE3 Ti = T_end;
NavStated match;
// 根据pt.time查找时间,pt.time是该点打到的时间与雷达开始时间之差,单位为毫秒
math::PoseInterp<NavStated>(
measures_.lidar_begin_time_ + pt.time * 1e-3, imu_states_, [](const NavStated &s) { return s.timestamp_; },
[](const NavStated &s) { return s.GetSE3(); }, Ti, match);
Vec3d pi = ToVec3d(pt);
Vec3d p_compensate = TIL_.inverse() * T_end.inverse() * Ti * TIL_ * pi;
pt.x = p_compensate(0);
pt.y = p_compensate(1);
pt.z = p_compensate(2);
});
scan_undistort_ = cloud;
if (options_.save_motion_undistortion_pcd_) {
sad::SaveCloudToFile("./data/ch7/after_undist.pcd", *cloud);
}
}
配准
Align()
void LooselyLIO::Align() {
FullCloudPtr scan_undistort_trans(new FullPointCloudType);
pcl::transformPointCloud(*scan_undistort_, *scan_undistort_trans, TIL_.matrix());
scan_undistort_ = scan_undistort_trans;
auto current_scan = ConvertToCloud<FullPointType>(scan_undistort_);
// voxel 之
pcl::VoxelGrid<PointType> voxel;
voxel.setLeafSize(0.5, 0.5, 0.5);
voxel.setInputCloud(current_scan);
CloudPtr current_scan_filter(new PointCloudType);
voxel.filter(*current_scan_filter);
/// 处理首帧雷达数据
if (flg_first_scan_) {
SE3 pose;
inc_ndt_lo_->AddCloud(current_scan_filter, pose);
flg_first_scan_ = false;
return;
}
/// 从EKF中获取预测pose,放入LO,获取LO位姿,最后合入EKF
SE3 pose_predict = eskf_.GetNominalSE3();
inc_ndt_lo_->AddCloud(current_scan_filter, pose_predict, true);
pose_of_lo_ = pose_predict;
eskf_.ObserveSE3(pose_of_lo_, 1e-2, 1e-2);
if (options_.with_ui_) {
// 放入UI
ui_->UpdateScan(current_scan, eskf_.GetNominalSE3()); // 转成Lidar Pose传给UI
ui_->UpdateNavState(eskf_.GetNominalState());
}
frame_num_++;
}
参考资料:
1、书籍:《自动驾驶与机器人中的SLAM技术:从理论到实践》
2、代码:https://github.com/gaoxiang12/slam_in_autonomous_driving/tree/master/src/ch7