0. 寫在最前面
本文持續更新位址:https://haoqchen.site/2018/05/06/amcl-code/
這篇文章記錄下自己在閱讀amcl源碼過程中的一些了解,如有不妥,歡迎評論或私信。
本文中所有代碼因為篇幅等問題,都隻給出主要部分,詳細的自己下載下傳下來對照着看。
如果覺得寫得還不錯,就請收藏一下啦~~~也可以找一下我寫的其他包的源碼解讀來看一下。關注一下我的專欄什麼的。
幫我github page點個 Star呗~~~
1. amcl是幹什麼的
Amcl is a probabilistic localization system for a robot moving in 2D. It implements the adaptive (or KLD-sampling) Monte Carlo localization approach (as described by Dieter Fox), which uses a particle filter to track the pose of a robot against a known map.
以上是官網的介紹,說白了就是2D的機率定位系統,輸入雷射雷達資料、裡程計資料,輸出機器人在地圖中的位姿。用的是自适應蒙特卡洛定位方法,這個方法是在已知地圖中使用粒子濾波方法得到位姿的。
如下圖所示,如果裡程計沒有誤差,完美的情況下,我們可以直接使用裡程計資訊(上半圖)推算出機器人(base_frame)相對裡程計坐标系的位置。但現實情況,裡程計存在漂移以及無法忽略的累計誤差,是以AMCL采用下半圖的方法,即先根據裡程計資訊初步定位base_frame,然後通過測量模型得到base_frame相對于map_frame(全局地圖坐标系),也就知道了機器人在地圖中的位姿。(注意,這裡雖然估計的是base到map的轉換,但最後釋出的是map到odom的轉換,可以了解為裡程計的漂移。)

2. 總體情況
2.1 CMakeLists
研究一個ROS包,肯定要先看CMakeLists。我們可以看到,這個包會生成
三個庫:
- amcl_pf
- amcl_map
-
amcl_sensors
一個節點:
- amcl
2.2 其中amcl的訂閱與釋出:
釋出話題:
- amcl_pose: geometry_msgs::PoseWithCovarianceStamped,後驗位姿+一個6*6的協方差矩陣(xyz+三個轉角)
- particlecloud:geometry_msgs::PoseArray,粒子位姿的數組
- 一個15秒的定時器:AmclNode::checkLaserReceived,檢查上一次收到雷射雷達資料至今是否超過15秒,如超過則報錯。
釋出服務:
- global_localization:&AmclNode::globalLocalizationCallback,這裡是沒有給定初始位姿的情況下在全局範圍内初始化粒子位姿,該Callback調用pf_init_model,然後調用AmclNode::uniformPoseGenerator在地圖的free點随機生成pf->max_samples個粒子
- request_nomotion_update:&AmclNode::nomotionUpdateCallback沒運動模型更新的情況下也暫時更新粒子群
- set_map:&AmclNode::setMapCallback
- dynamic_reconfigure::Server動态參數配置器。
訂閱話題:
- scan_topic_:sensor_msgs::LaserScan,AmclNode::laserReceived,這裡是tf訂閱,轉換到odom_frame_id_
- initialpose:AmclNode::initialPoseReceived,這個應該就是訂閱rviz中給的初始化位姿,調用AmclNode::handleInitialPoseMessage,隻接受global_frame_id_(一般為map)的坐标,并重新生成粒子
- map:AmclNode::mapReceived這個在use_map_topic_的時候才訂閱,否則requestMap();我這裡也沒有訂閱,因為隻使用了一個固定的地圖。
3. amcl_node.cpp
這個檔案實作了上述的amcl節點功能。
3.1 main
int main(int argc, char** argv)
{
ros::init(argc, argv, "amcl");
ros::NodeHandle nh;
// Override default sigint handler
signal(SIGINT, sigintHandler);
// Make our node available to sigintHandler
amcl_node_ptr.reset(new AmclNode());
if (argc == 1)
{
// run using ROS input
ros::spin();
}
}
沒啥好說的,主要就是定義了amcl節點,初始化了一個AmclNode的類對象,最關鍵的中斷函數配置都在該類的構造函數中實作。
3.2 AmclNode
AmclNode::AmclNode()
{
boost::recursive_mutex::scoped_lock l(configuration_mutex_);
//測量模型選擇,具體參考《機率機器人》第6章内容,這裡預設的likelihood_field是6.4節的似然域模型
std::string tmp_model_type;
private_nh_.param("laser_model_type", tmp_model_type, std::string("likelihood_field"));
//機器人模型選擇,具體參考《機率機器人》5.4節裡程計運動模型内容,這裡預設的diff應該是差分型(兩輪驅動)的機器人?,另外一個是全向機器人?
private_nh_.param("odom_model_type", tmp_model_type, std::string("diff"));
//從參數伺服器中擷取初始位姿及初始分布
updatePoseFromServer();
//定義話題及訂閱等,具體在本文第2章有講
pose_pub_ = nh_.advertise<geometry_msgs::PoseWithCovarianceStamped>("amcl_pose", 2, true);
}
3.3 requestMap
這裡請求服務static_server提供map,然後調用handleMapMessage處理地圖資訊。這裡的地圖類型是nav_msgs::OccupancyGrid,這裡不介紹,有興趣自己去看資料類型。然後擷取初始位姿、初始化粒子、裡程計資訊等。
AmclNode::requestMap()
{
//一直請求服務static_map直到成功,該服務在map_server這個包的map_server節點中進行定義
while(!ros::service::call("static_map", req, resp))
{
ROS_WARN("Request for map failed; trying again...");
ros::Duration d(0.5);
d.sleep();
}
handleMapMessage( resp.map );
}
AmclNode::handleMapMessage(const nav_msgs::OccupancyGrid& msg)
{
//free相應的指針
freeMapDependentMemory();
//轉換成标準地圖,0->-1(不是障礙);100->+1(障礙);else->0(不明)
map_ = convertMap(msg);
//将不是障礙的點的坐标儲存下來
#if NEW_UNIFORM_SAMPLING
// Index of free space
free_space_indices.resize(0);
for(int i = 0; i < map_->size_x; i++)
for(int j = 0; j < map_->size_y; j++)
if(map_->cells[MAP_INDEX(map_,i,j)].occ_state == -1)
free_space_indices.push_back(std::make_pair(i,j));
#endif
// Create the particle filter,定義了一個回調,尚未清除幹啥
pf_ = pf_alloc(min_particles_, max_particles_,
alpha_slow_, alpha_fast_,
(pf_init_model_fn_t)AmclNode::uniformPoseGenerator,
(void *)map_);
// 從參數伺服器擷取初始位姿及方差放到pf中
updatePoseFromServer();
// 定義裡程計與雷射雷達并初始化資料
// Odometry
delete odom_;
odom_ = new AMCLOdom();
ROS_ASSERT(odom_);
odom_->SetModel( odom_model_type_, alpha1_, alpha2_, alpha3_, alpha4_, alpha5_ );
// Laser
delete laser_;
laser_ = new AMCLLaser(max_beams_, map_);
// In case the initial pose message arrived before the first map,
// try to apply the initial pose now that the map has arrived.
applyInitialPose();
}
3.4 laserReceived
其中變量pose是base相對于odom的位姿;pf_odom_pose_則是上一時刻base相對于odom的位姿,用于後續得到機器人的相對運動程度。
AmclNode::laserReceived(const sensor_msgs::LaserScanConstPtr& laser_scan)
{
// Do we have the base->base_laser Tx yet?感覺這裡是支援多個雷射雷達的,找到目前響應的雷射雷達之前存儲的資訊,
//如相對于base的轉換,是否更新等,使用map結構直接通過id來找到對應序号即可
//如果之前沒有備案則在map結構中備案,然後存到frame_to_laser_及lasers_中下次備用
if(frame_to_laser_.find(laser_scan->header.frame_id) == frame_to_laser_.end())
{
frame_to_laser_[laser_scan->header.frame_id] = laser_index;
} else {
// we have the laser pose, retrieve laser index
laser_index = frame_to_laser_[laser_scan->header.frame_id];
}
// Where was the robot when this scan was taken?獲得base在雷射雷達掃描時候相對于odom的相對位姿
pf_vector_t pose;
if(!getOdomPose(latest_odom_pose_, pose.v[0], pose.v[1], pose.v[2],
laser_scan->header.stamp, base_frame_id_))
{
ROS_ERROR("Couldn't determine robot's pose associated with laser scan");
return;
}
pf_vector_t delta = pf_vector_zero();
//如果不是第一幀,看運動幅度是否超過設定值需要更新(第一幀是指更新了地圖或者更新初始位姿)
if(pf_init_)
{
// Compute change in pose
//delta = pf_vector_coord_sub(pose, pf_odom_pose_);
delta.v[0] = pose.v[0] - pf_odom_pose_.v[0];
delta.v[1] = pose.v[1] - pf_odom_pose_.v[1];
delta.v[2] = angle_diff(pose.v[2], pf_odom_pose_.v[2]);
// See if we should update the filter
bool update = fabs(delta.v[0]) > d_thresh_ ||
fabs(delta.v[1]) > d_thresh_ ||
fabs(delta.v[2]) > a_thresh_;
update = update || m_force_update;
m_force_update=false;
// Set the laser update flags
if(update)
for(unsigned int i=0; i < lasers_update_.size(); i++)
lasers_update_[i] = true;
}
//第一幀則初始化一些值
bool force_publication = false;
if(!pf_init_)
{
// Pose at last filter update
pf_odom_pose_ = pose;
// Filter is now initialized
pf_init_ = true;
}
// If the robot has moved, update the filter
else if(pf_init_ && lasers_update_[laser_index])//如果已經初始化并需要更新則更新運動模型
{
//這是amcl_odom.cpp中最重要的一個函數,實作了用運動模型來更新現有的每一個粒子的位姿(這裡得到的隻是目前時刻的先驗位姿)
// Use the action data to update the filter
odom_->UpdateAction(pf_, (AMCLSensorData*)&odata);
}
bool resampled = false;
// If the robot has moved, update the filter
if(lasers_update_[laser_index])
{//接下來一大片都是對原始雷射雷達資料進行處理,轉換到AMCLLaserData。包括角度最小值、增量到base_frame的轉換、測距距離最大值、最小值。
//具體看代碼,篇幅限制不詳細列了
try
{
tf_->transformQuaternion(base_frame_id_, min_q, min_q);
tf_->transformQuaternion(base_frame_id_, inc_q, inc_q);
}
ldata.range_max = std::min(laser_scan->range_max, (float)laser_max_range_);
range_min = std::max(laser_scan->range_min, (float)laser_min_range_);
ldata.ranges = new double[ldata.range_count][2];
for(int i=0;i<ldata.range_count;i++)
{
// amcl doesn't (yet) have a concept of min range. So we'll map short
// readings to max range.//雷射雷達傳上來的資料隻标記了最大值最小值,但是沒做處理,直接将原始資料傳上來,
if(laser_scan->ranges[i] <= range_min)//這裡将最小值當最大值處理,因為在類似likelihood_field模型中,會直接将最大值丢棄
ldata.ranges[i][0] = ldata.range_max;
}
//注意這裡是amcl_laser.cpp的UpdateSensor,不是amcl_sensor.cpp的。通過判斷前面設定的測量模型調用pf_update_sensor,
//該函數需要傳入相應模型的處理函數,這裡所有的測量模型在《機率機器人》的第六章有詳細講,具體自己看,後面隻針對自己使用的likelihood_field模型講一下
//pf_update_sensor實作對所有粒子更新權重,并歸一化、計算出《機率機器人》8.3.5的失效恢複的長期似然和短期似然
lasers_[laser_index]->UpdateSensor(pf_, (AMCLSensorData*)&ldata);
lasers_update_[laser_index] = false;
pf_odom_pose_ = pose;
//多少次雷射雷達回調之後進行重采樣呗,我這裡resample_interval_=0.5,隻有一個雷射雷達,每次都更新。
// Resample the particles
if(!(++resample_count_ % resample_interval_))
{
//按照一定的規則重采樣粒子,包括前面說的失效恢複、粒子權重等,然後放入到kdtree,暫時先了解成關于位姿的二叉樹,
//然後進行聚類,得到均值和方差等資訊,個人了解就是将相近的一堆粒子融合成一個粒子了,沒必要維持太多相近的@TODO
pf_update_resample(pf_);
resampled = true;
}
// Publish the resulting cloud
// TODO: set maximum rate for publishing
if (!m_force_update) {
//将新粒子釋出到全局坐标系下,一般是map
particlecloud_pub_.publish(cloud_msg);
}
}
if(resampled || force_publication)
{
//周遊所有粒子簇,找出權重均值最大的簇,其平均位姿就是我們要求的機器人後驗位姿,到此一次循環已經所有完成
for(int hyp_count = 0;
hyp_count < pf_->sets[pf_->current_set].cluster_count; hyp_count++)
{
if (!pf_get_cluster_stats(pf_, hyp_count, &weight, &pose_mean, &pose_cov))
{
break;
}
hyps[hyp_count].weight = weight;
hyps[hyp_count].pf_pose_mean = pose_mean;
hyps[hyp_count].pf_pose_cov = pose_cov;
if(hyps[hyp_count].weight > max_weight)
{
max_weight = hyps[hyp_count].weight;
max_weight_hyp = hyp_count;
}
}
//将位姿、粒子集、協方差矩陣等進行更新、釋出
if(max_weight > 0.0)
{
geometry_msgs::PoseWithCovarianceStamped p;
// Fill in the header
p.header.frame_id = global_frame_id_;
p.header.stamp = laser_scan->header.stamp;
// Copy in the pose
p.pose.pose.position.x = hyps[max_weight_hyp].pf_pose_mean.v[0];
p.pose.pose.position.y = hyps[max_weight_hyp].pf_pose_mean.v[1];
tf::quaternionTFToMsg(tf::createQuaternionFromYaw(hyps[max_weight_hyp].pf_pose_mean.v[2]),
p.pose.pose.orientation);
// Copy in the covariance, converting from 3-D to 6-D
pf_sample_set_t* set = pf_->sets + pf_->current_set;
for(int i=0; i<2; i++)
{
for(int j=0; j<2; j++)
{
p.pose.covariance[6*i+j] = set->cov.m[i][j];
}
}
p.pose.covariance[6*5+5] = set->cov.m[2][2];
pose_pub_.publish(p);
last_published_pose = p;
//這裡就是所說的,map~base減去odom~base得到map~odom,最後釋出的是map~odom。
// subtracting base to odom from map to base and send map to odom instead
tf::Stamped<tf::Pose> odom_to_map;
try
{
tf::Transform tmp_tf(tf::createQuaternionFromYaw(hyps[max_weight_hyp].pf_pose_mean.v[2]),
tf::Vector3(hyps[max_weight_hyp].pf_pose_mean.v[0],
hyps[max_weight_hyp].pf_pose_mean.v[1],
0.0));
tf::Stamped<tf::Pose> tmp_tf_stamped (tmp_tf.inverse(),
laser_scan->header.stamp,
base_frame_id_);
this->tf_->transformPose(odom_frame_id_,
tmp_tf_stamped,
odom_to_map);
}
catch(tf::TransformException)
{
ROS_DEBUG("Failed to subtract base to odom transform");
return;
}
latest_tf_ = tf::Transform(tf::Quaternion(odom_to_map.getRotation()),
tf::Point(odom_to_map.getOrigin()));
latest_tf_valid_ = true;
if (tf_broadcast_ == true)
{
// We want to send a transform that is good up until a
// tolerance time so that odom can be used
ros::Time transform_expiration = (laser_scan->header.stamp +
transform_tolerance_);
tf::StampedTransform tmp_tf_stamped(latest_tf_.inverse(),
transform_expiration,
global_frame_id_, odom_frame_id_);
this->tfb_->sendTransform(tmp_tf_stamped);
sent_first_transform_ = true;
}
}
else
{
ROS_ERROR("No pose!");
}
}
else if(latest_tf_valid_)//if(resampled || force_publication)
{
}
}
3.5 AMCLOdom::UpdateAction
首先擷取上一幀的位姿,然後根據自己前面選擇的機器人模型進行運動模型更新。這裡由于我自己的模型是ODOM_MODEL_DIFF,是以隻針對這個模型進行講解,剩下的原理應該一樣,細節上有差別,自己看。代碼中有講到,利用到的算法是《Probabilistic Robotics 2》136頁的内容,如果是Probabilistic Robotics 1或者中文版其實就是5.4節中的采樣算法,這裡不再細述。
我直白一點的了解就是,已知上一時刻所有粒子的後驗位姿、上一時刻到現在兩個輪子的編碼器值,給所有粒子随機采樣目前時刻的先驗位姿。為什麼是随機,不是确定的直接從編碼器計算得到呢?因為編碼器等存在誤差,是以采樣到的每個粒子的先驗位姿都是不确定的,但理論上是服從一個分布的(這裡是正态,書本上給出三種分布),跟實際運動有關,如果運動越大、則采樣到的跟直接裡程計計算得到的值就越有可能相差的比較大(算法上表示為方差越大)。
// Apply the action model
bool AMCLOdom::UpdateAction(pf_t *pf, AMCLSensorData *data)
{
set = pf->sets + pf->current_set;
pf_vector_t old_pose = pf_vector_sub(ndata->pose, ndata->delta);
switch( this->model_type )
{
case ODOM_MODEL_DIFF:
{
// Implement sample_motion_odometry (Prob Rob p 136)
double delta_rot1, delta_trans, delta_rot2;
double delta_rot1_hat, delta_trans_hat, delta_rot2_hat;
double delta_rot1_noise, delta_rot2_noise;
//計算裡程計得到的ut,即用旋轉、直線運動和另一個旋轉來表示相對運動
// Avoid computing a bearing from two poses that are extremely near each
// other (happens on in-place rotation).
if(sqrt(ndata->delta.v[1]*ndata->delta.v[1] + //如果原地旋轉的情況,定義第一個旋轉為0,認為所有旋轉都是第二個旋轉做的
ndata->delta.v[0]*ndata->delta.v[0]) < 0.01)
delta_rot1 = 0.0;
else
delta_rot1 = angle_diff(atan2(ndata->delta.v[1], ndata->delta.v[0]),
old_pose.v[2]);
delta_trans = sqrt(ndata->delta.v[0]*ndata->delta.v[0] +
ndata->delta.v[1]*ndata->delta.v[1]);
delta_rot2 = angle_diff(ndata->delta.v[2], delta_rot1);
//這裡作者比書本多考慮了一種情況,就是機器人旋轉小角度,然後往後走的情況,比如旋轉-20°,然後往後走,計算出來可能是160°,
//但實際隻轉了20°。按書本可能是比較大的方差(160°),不太準确,但這裡的話還是按照比較小的方差來采樣。
//但我表示懷疑,萬一真的轉了160°再前向運動的話怎麼辦?還是用比較小的方差來采樣,可能會使得采樣的準确率下降。
//實際中,編碼器采樣率很高,且設定了一個很小的變化角度就進行運動模型更新,是以我說的這種情況幾乎不會發生
// We want to treat backward and forward motion symmetrically for the
// noise model to be applied below. The standard model seems to assume
// forward motion.
delta_rot1_noise = std::min(fabs(angle_diff(delta_rot1,0.0)),
fabs(angle_diff(delta_rot1,M_PI)));
delta_rot2_noise = std::min(fabs(angle_diff(delta_rot2,0.0)),
fabs(angle_diff(delta_rot2,M_PI)));
//對每個粒子用書本的方法進行更新
for (int i = 0; i < set->sample_count; i++)
{
pf_sample_t* sample = set->samples + i;
// Sample pose differences
delta_rot1_hat = angle_diff(delta_rot1,
pf_ran_gaussian(this->alpha1*delta_rot1_noise*delta_rot1_noise +
this->alpha2*delta_trans*delta_trans));
delta_trans_hat = delta_trans -
pf_ran_gaussian(this->alpha3*delta_trans*delta_trans +
this->alpha4*delta_rot1_noise*delta_rot1_noise +
this->alpha4*delta_rot2_noise*delta_rot2_noise);
delta_rot2_hat = angle_diff(delta_rot2,
pf_ran_gaussian(this->alpha1*delta_rot2_noise*delta_rot2_noise +
this->alpha2*delta_trans*delta_trans));
// Apply sampled update to particle pose
sample->pose.v[0] += delta_trans_hat *
cos(sample->pose.v[2] + delta_rot1_hat);
sample->pose.v[1] += delta_trans_hat *
sin(sample->pose.v[2] + delta_rot1_hat);
sample->pose.v[2] += delta_rot1_hat + delta_rot2_hat;
}
}
}
return true;
}
3.6 LikelihoodFieldModel
算法原理請參看《機率機器人》6.4節,這裡會給每個粒子(代表一個位姿)計算機率、更新機率,然後傳回所有粒子機率的總和。
double AMCLLaser::LikelihoodFieldModel(AMCLLaserData *data, pf_sample_set_t* set)
{
// Compute the sample weights周遊所有雷射雷達資料點,
for (j = 0; j < set->sample_count; j++)
{
sample = set->samples + j;
pose = sample->pose;//周遊每個粒子,這是粒子對應的位姿,是經運動模型更新後先驗位姿
// Take account of the laser pose relative to the robot
//這個應該是通過機器人與全局坐标系的位姿(每個粒子的位姿),計算雷射雷達相對于全局坐标系的位姿。友善後續将雷射雷達掃描的終點轉換到全局坐标系
//點進去pf_vector_coord_add這個函數,b對應的就是《機率機器人》P128,6.4第一個公式中的機器人在t時刻的位姿,a代表“與機器人固連的傳感器局部坐标系位置”
pose = pf_vector_coord_add(self->laser_pose, pose);//這裡的laser_pose其實是laserReceived函數一開始初始化每個雷射雷達時得到的雷射雷達在base中的位姿
p = 1.0;
// Pre-compute a couple of things
double z_hit_denom = 2 * self->sigma_hit * self->sigma_hit;//測量噪聲的方差
double z_rand_mult = 1.0/data->range_max;//無法解釋的随機測量的分母
//因為時間問題,并不是全部點都進行似然計算的,這裡隻是間隔地選點,我這裡設定的是一共選擇60個點~~~
step = (data->range_count - 1) / (self->max_beams - 1);
for (i = 0; i < data->range_count; i += step)
{
obs_range = data->ranges[i][0];
obs_bearing = data->ranges[i][1];
//繼續上面《機率機器人》P128的後半段公式,得到雷射雷達點最遠端的世界坐标
// Compute the endpoint of the beam
hit.v[0] = pose.v[0] + obs_range * cos(pose.v[2] + obs_bearing);
hit.v[1] = pose.v[1] + obs_range * sin(pose.v[2] + obs_bearing);
//轉換到栅格坐标
// Convert to map grid coords.
int mi, mj;
mi = MAP_GXWX(self->map, hit.v[0]);
mj = MAP_GYWY(self->map, hit.v[1]);
//這是提前計算好的最近距離,計算函數在map_cspace.cpp的map_update_cspace中實作周遊計算,該函數是被AMCLLaser::SetModelLikelihoodField調用
//而這個函數則隻在AmclNode::handleMapMessage中調用???而這個handle在多個地方調用,暫時未清楚@TODO
// Part 1: Get distance from the hit to closest obstacle.
// Off-map penalized as max distance
if(!MAP_VALID(self->map, mi, mj))
z = self->map->max_occ_dist;
else
z = self->map->cells[MAP_INDEX(self->map,mi,mj)].occ_dist;
//書本算法流程的公式
// Gaussian model
// NOTE: this should have a normalization of 1/(sqrt(2pi)*sigma)
pz += self->z_hit * exp(-(z * z) / z_hit_denom);
// Part 2: random measurements
pz += self->z_rand * z_rand_mult;
// TODO: outlier rejection for short readings
assert(pz <= 1.0);
assert(pz >= 0.0);
// p *= pz;
// here we have an ad-hoc weighting scheme for combining beam probs
// works well, though...
p += pz*pz*pz;//這裡有點不太懂。。。理論上應該是上面注釋掉的公式。這裡應該是為了防止噪聲,比如某個出現錯誤的0,其餘都不管用了
}//每個粒子的所有雷射雷達點循環
sample->weight *= p;
total_weight += sample->weight;
}//粒子循環
return(total_weight);
}
參考
https://wenku.baidu.com/view/92a45164a9956bec0975f46527d3240c8447a1d5.html
https://www.cnblogs.com/lysuns/articles/4710712.html
ROS官網參數配置
喜歡我的文章的話Star一下呗Star
版權聲明:本文為白夜行的狼原創文章,未經允許不得以任何形式轉載