从零开始搭二维激光SLAM --- 基于PL-ICP的激光雷达里程计
首先ROS中的坐标系包括
map: 地图坐标系,也被称为世界坐标系,是静止不动的
odom: 里程计坐标系,相对于map来说一般情况下是静止的,有些情况下会变动(定位节点为了修正机器人的位姿从而改变了map->odom间的坐标变换)
base_link: 代表机器人的旋转中心的坐标系,相对于odom来说base_link是运动的
laser_link: 激光雷达的坐标系,相对于base_link来说是静止的,因为雷达装在机器人上,雷达不会自己飞起来
他们的关系是单方面依赖的
map -> odom -> base_link -> laser_link 这种关系被称为TF树可以使用rqt显示
ROS中管理坐标系的库TF2
tf2常用数据类型与常用函数汇总:tf2常用数据类型与常用函数汇总_李太白lx的博客-CSDN博客
//新增函数
void InitParams();//初始化参数
bool GetBaseToLaserTf(const std::string &frame_id);//机器人和雷达的变换
void GetPrediction(double &prediction_change_x, double &prediction_change_y, double// &prediction_change_angle, double dt);//
void CreateTfFromXYTheta(double x, double y, double theta, tf2::Transform& t);//
void PublishTFAndOdometry();//
bool NewKeyframeNeeded(const tf2::Transform &d);//
新增的代码如下
构造函数(一般用来初始化参数)
private_node_ 初始化为 “~” ,代表私有命名空间,可以用来获取节点内的参数
ScanMatchPLICP::ScanMatchPLICP() : private_node_("~"), tf_listener_(tfBuffer_)
{
// \033[1;32m,\033[0m 终端显示成绿色
ROS_INFO_STREAM("\033[1;32m----> PLICP odometry started.\033[0m");
laser_scan_subscriber_ = node_handle_.subscribe(
"laser_scan", 1, &ScanMatchPLICP::ScanCallback, this);
odom_publisher_ = node_handle_.advertise<nav_msgs::Odometry>("odom_plicp", 50);
// 参数初始化
InitParams();
// 第一帧雷达还未到来
initialized_ = false;
base_in_odom_.setIdentity();
base_in_odom_keyframe_.setIdentity();
input_.laser[0] = 0.0;
input_.laser[1] = 0.0;
input_.laser[2] = 0.0;
// Initialize output_ vectors as Null for error-checking
output_.cov_x_m = 0;
output_.dx_dy1_m = 0;
output_.dx_dy2_m = 0;
}
InitParams()
/*
* ros与csm的参数初始化
*/
void ScanMatchPLICP::InitParams()
{
private_node_.param<std::string>("odom_frame", odom_frame_, "odom");
private_node_.param<std::string>("base_frame", base_frame_, "base_link");
// **** keyframe params: when to generate the keyframe scan
// if either is set to 0, reduces to frame-to-frame matching
private_node_.param<double>("kf_dist_linear", kf_dist_linear_, 0.1);
private_node_.param<double>("kf_dist_angular", kf_dist_angular_, 5.0 * (M_PI / 180.0));
kf_dist_linear_sq_ = kf_dist_linear_ * kf_dist_linear_;
// **** CSM 的参数 - comments copied from algos.h (by Andrea Censi)
// Maximum angular displacement between scans
if (!private_node_.getParam("max_angular_correction_deg", input_.max_angular_correction_deg))
input_.max_angular_correction_deg = 45.0;
// Maximum translation between scans (m)
if (!private_node_.getParam("max_linear_correction", input_.max_linear_correction))
input_.max_linear_correction = 0.50;
// Maximum ICP cycle iterations
if (!private_node_.getParam("max_iterations", input_.max_iterations))
input_.max_iterations = 10;
// A threshold for stopping (m)
if (!private_node_.getParam("epsilon_xy", input_.epsilon_xy))
input_.epsilon_xy = 0.000001;
// A threshold for stopping (rad)
if (!private_node_.getParam("epsilon_theta", input_.epsilon_theta))
input_.epsilon_theta = 0.000001;
// Maximum distance for a correspondence to be valid
if (!private_node_.getParam("max_correspondence_dist", input_.max_correspondence_dist))
input_.max_correspondence_dist = 0.3;
// Noise in the scan (m)
if (!private_node_.getParam("sigma", input_.sigma))
input_.sigma = 0.010;
// Use smart tricks for finding correspondences.
if (!private_node_.getParam("use_corr_tricks", input_.use_corr_tricks))
input_.use_corr_tricks = 1;
// Restart: Restart if error is over threshold
if (!private_node_.getParam("restart", input_.restart))
input_.restart = 0;
// Restart: Threshold for restarting
if (!private_node_.getParam("restart_threshold_mean_error", input_.restart_threshold_mean_error))
input_.restart_threshold_mean_error = 0.01;
// Restart: displacement for restarting. (m)
if (!private_node_.getParam("restart_dt", input_.restart_dt))
input_.restart_dt = 1.0;
// Restart: displacement for restarting. (rad)
if (!private_node_.getParam("restart_dtheta", input_.restart_dtheta))
input_.restart_dtheta = 0.1;
// Max distance for staying in the same clustering
if (!private_node_.getParam("clustering_threshold", input_.clustering_threshold))
input_.clustering_threshold = 0.25;
// Number of neighbour rays used to estimate the orientation
if (!private_node_.getParam("orientation_neighbourhood", input_.orientation_neighbourhood))
input_.orientation_neighbourhood = 20;
// If 0, it's vanilla ICP
if (!private_node_.getParam("use_point_to_line_distance", input_.use_point_to_line_distance))
input_.use_point_to_line_distance = 1;
// Discard correspondences based on the angles
if (!private_node_.getParam("do_alpha_test", input_.do_alpha_test))
input_.do_alpha_test = 0;
// Discard correspondences based on the angles - threshold angle, in degrees
if (!private_node_.getParam("do_alpha_test_thresholdDeg", input_.do_alpha_test_thresholdDeg))
input_.do_alpha_test_thresholdDeg = 20.0;
// Percentage of correspondences to consider: if 0.9,
// always discard the top 10% of correspondences with more error
if (!private_node_.getParam("outliers_maxPerc", input_.outliers_maxPerc))
input_.outliers_maxPerc = 0.90;
// Parameters describing a simple adaptive algorithm for discarding.
// 1) Order the errors.
// 2) Choose the percentile according to outliers_adaptive_order.
// (if it is 0.7, get the 70% percentile)
// 3) Define an adaptive threshold multiplying outliers_adaptive_mult
// with the value of the error at the chosen percentile.
// 4) Discard correspondences over the threshold.
// This is useful to be conservative; yet remove the biggest errors.
if (!private_node_.getParam("outliers_adaptive_order", input_.outliers_adaptive_order))
input_.outliers_adaptive_order = 0.7;
if (!private_node_.getParam("outliers_adaptive_mult", input_.outliers_adaptive_mult))
input_.outliers_adaptive_mult = 2.0;
// If you already have a guess of the solution, you can compute the polar angle
// of the points of one scan in the new position. If the polar angle is not a monotone
// function of the readings index, it means that the surface is not visible in the
// next position. If it is not visible, then we don't use it for matching.
if (!private_node_.getParam("do_visibility_test", input_.do_visibility_test))
input_.do_visibility_test = 0;
// no two points in laser_sens can have the same corr.
if (!private_node_.getParam("outliers_remove_doubles", input_.outliers_remove_doubles))
input_.outliers_remove_doubles = 1;
// If 1, computes the covariance of ICP using the method http://purl.org/censi/2006/icpcov
if (!private_node_.getParam("do_compute_covariance", input_.do_compute_covariance))
input_.do_compute_covariance = 0;
// Checks that find_correspondences_tricks gives the right answer
if (!private_node_.getParam("debug_verify_tricks", input_.debug_verify_tricks))
input_.debug_verify_tricks = 0;
// If 1, the field 'true_alpha' (or 'alpha') in the first scan is used to compute the
// incidence beta, and the factor (1/cos^2(beta)) used to weight the correspondence.");
if (!private_node_.getParam("use_ml_weights", input_.use_ml_weights))
input_.use_ml_weights = 0;
// If 1, the field 'readings_sigma' in the second scan is used to weight the
// correspondence by 1/sigma^2
if (!private_node_.getParam("use_sigma_weights", input_.use_sigma_weights))
input_.use_sigma_weights = 0;
}
GetBaseToLaserTf()
/**
* 获取机器人坐标系与雷达坐标系间的坐标变换
*/
bool ScanMatchPLICP::GetBaseToLaserTf(const std::string &frame_id)
{
ros::Time t = ros::Time::now();
geometry_msgs::TransformStamped transformStamped;
// 获取tf并不是瞬间就能获取到的,要给1秒的缓冲时间让其找到tf
try
{
transformStamped = tfBuffer_.lookupTransform(base_frame_, frame_id,
t, ros::Duration(1.0));
}
catch (tf2::TransformException &ex)
{
ROS_WARN("%s", ex.what());
ros::Duration(1.0).sleep();
return false;
}
// 将获取的tf存到base_to_laser_中
tf2::fromMsg(transformStamped.transform, base_to_laser_);
laser_to_base_ = base_to_laser_.inverse();
return true;
}
ScanMatchWithPLICP()
实现PLICP匹配的
/**
* 使用PLICP进行帧间位姿的计算
*/
void ScanMatchPLICP::ScanMatchWithPLICP(LDP &curr_ldp_scan, const ros::Time &time)
{
// CSM is used in the following way:
// The scans are always in the laser frame
// The reference scan (prevLDPcan_) has a pose of [0, 0, 0]
// The new scan (currLDPScan) has a pose equal to the movement
// of the laser in the laser frame since the last scan
// The computed correction is then propagated using the tf machinery
prev_ldp_scan_->odometry[0] = 0.0;
prev_ldp_scan_->odometry[1] = 0.0;
prev_ldp_scan_->odometry[2] = 0.0;
prev_ldp_scan_->estimate[0] = 0.0;
prev_ldp_scan_->estimate[1] = 0.0;
prev_ldp_scan_->estimate[2] = 0.0;
prev_ldp_scan_->true_pose[0] = 0.0;
prev_ldp_scan_->true_pose[1] = 0.0;
prev_ldp_scan_->true_pose[2] = 0.0;
input_.laser_ref = prev_ldp_scan_;
input_.laser_sens = curr_ldp_scan;
// 匀速模型,速度乘以时间,得到预测的odom坐标系下的位姿变换
double dt = (time - last_icp_time_).toSec();
double pr_ch_x, pr_ch_y, pr_ch_a;
GetPrediction(pr_ch_x, pr_ch_y, pr_ch_a, dt);
tf2::Transform prediction_change;
CreateTfFromXYTheta(pr_ch_x, pr_ch_y, pr_ch_a, prediction_change);
// account for the change since the last kf, in the fixed frame
// 将odom坐标系下的坐标变换 转换成 base_in_odom_keyframe_坐标系下的坐标变换
prediction_change = prediction_change * (base_in_odom_ * base_in_odom_keyframe_.inverse());
// the predicted change of the laser's position, in the laser frame
// 将base_link坐标系下的坐标变换 转换成 雷达坐标系下的坐标变换
tf2::Transform prediction_change_lidar;
prediction_change_lidar = laser_to_base_ * base_in_odom_.inverse() * prediction_change * base_in_odom_ * base_to_laser_;
input_.first_guess[0] = prediction_change_lidar.getOrigin().getX();
input_.first_guess[1] = prediction_change_lidar.getOrigin().getY();
input_.first_guess[2] = tf2::getYaw(prediction_change_lidar.getRotation());
// If they are non-Null, free covariance gsl matrices to avoid leaking memory
if (output_.cov_x_m)
{
gsl_matrix_free(output_.cov_x_m);
output_.cov_x_m = 0;
}
if (output_.dx_dy1_m)
{
gsl_matrix_free(output_.dx_dy1_m);
output_.dx_dy1_m = 0;
}
if (output_.dx_dy2_m)
{
gsl_matrix_free(output_.dx_dy2_m);
output_.dx_dy2_m = 0;
}
start_time_ = std::chrono::steady_clock::now();
// 调用csm进行plicp计算
sm_icp(&input_, &output_);
end_time_ = std::chrono::steady_clock::now();
time_used_ = std::chrono::duration_cast<std::chrono::duration<double>>(end_time_ - start_time_);
// std::cout << "PLICP计算用时: " << time_used_.count() << " 秒。" << std::endl;
tf2::Transform corr_ch;
if (output_.valid)
{
// 雷达坐标系下的坐标变换
tf2::Transform corr_ch_l;
CreateTfFromXYTheta(output_.x[0], output_.x[1], output_.x[2], corr_ch_l);
// 将雷达坐标系下的坐标变换 转换成 base_link坐标系下的坐标变换
corr_ch = base_to_laser_ * corr_ch_l * laser_to_base_;
// 更新 base_link 在 odom 坐标系下 的坐标
base_in_odom_ = base_in_odom_keyframe_ * corr_ch;
latest_velocity_.linear.x = corr_ch.getOrigin().getX() / dt;
latest_velocity_.angular.z = tf2::getYaw(corr_ch.getRotation()) / dt;
}
else
{
ROS_WARN("not Converged");
}
// 发布tf与odom话题
PublishTFAndOdometry();
// 检查是否需要更新关键帧坐标
if (NewKeyframeNeeded(corr_ch))
{
// 更新关键帧坐标
ld_free(prev_ldp_scan_);
prev_ldp_scan_ = curr_ldp_scan;
base_in_odom_keyframe_ = base_in_odom_;
}
else
{
ld_free(curr_ldp_scan);
}
last_icp_time_ = time;
}
GetPrediction()
/**
* 推测从上次icp的时间到当前时刻间的坐标变换
* 使用匀速模型,根据当前的速度,乘以时间,得到推测出来的位移
*/
void ScanMatchPLICP::GetPrediction(double &prediction_change_x,
double &prediction_change_y,
double &prediction_change_angle,
double dt)
{
// 速度小于 1e-6 , 则认为是静止的
prediction_change_x = latest_velocity_.linear.x < 1e-6 ? 0.0 : dt * latest_velocity_.linear.x;
prediction_change_y = latest_velocity_.linear.y < 1e-6 ? 0.0 : dt * latest_velocity_.linear.y;
prediction_change_angle = latest_velocity_.linear.z < 1e-6 ? 0.0 : dt * latest_velocity_.linear.z;
if (prediction_change_angle >= M_PI)
prediction_change_angle -= 2.0 * M_PI;
else if (prediction_change_angle < -M_PI)
prediction_change_angle += 2.0 * M_PI;
}
CreateTfFromXYTheta()
为 tf2::Transform 赋值
/**
* 从x,y,theta创建tf
*/
void ScanMatchPLICP::CreateTfFromXYTheta(double x, double y, double theta, tf2::Transform &t)
{
t.setOrigin(tf2::Vector3(x, y, 0.0));
tf2::Quaternion q;
q.setRPY(0.0, 0.0, theta);
t.setRotation(q);
}
PublishTFAndOdometry()
/**
* 发布tf与odom话题
*/
void ScanMatchPLICP::PublishTFAndOdometry()
{
geometry_msgs::TransformStamped tf_msg;
tf_msg.header.stamp = current_time_;
tf_msg.header.frame_id = odom_frame_;
tf_msg.child_frame_id = base_frame_;
tf_msg.transform = tf2::toMsg(base_in_odom_);
// 发布 odom 到 base_link 的 tf
tf_broadcaster_.sendTransform(tf_msg);
nav_msgs::Odometry odom_msg;
odom_msg.header.stamp = current_time_;
odom_msg.header.frame_id = odom_frame_;
odom_msg.child_frame_id = base_frame_;
tf2::toMsg(base_in_odom_, odom_msg.pose.pose);
odom_msg.twist.twist = latest_velocity_;
// 发布 odomemtry 话题
odom_publisher_.publish(odom_msg);
}
NewKeyframeNeeded()
/**
* 如果平移大于阈值,角度大于阈值,则创新新的关键帧
* @return 需要创建关键帧返回true, 否则返回false
*/
bool ScanMatchPLICP::NewKeyframeNeeded(const tf2::Transform &d)
{
if (fabs(tf2::getYaw(d.getRotation())) > kf_dist_angular_)
return true;
if (scan_count_++ == kf_scan_count_)
{
scan_count_ = 0;
return true;
}
double x = d.getOrigin().getX();
double y = d.getOrigin().getY();
if (x * x + y * y > kf_dist_linear_sq_)
return true;
return false;
}