(笔记)(4)AMCL包源码分析 | 传感器模型与sensor文件夹

 

AMCL包接收地图,建立运动模型使得粒子可以跟着机器人一起运动,建立观测模型对模拟机器人运动的粒子进行打分,并且建立相关的tf变换信息,最后输出位姿。接收地图部分在上一讲已讲解完毕,这一讲讲述运动模型和观测模型对AMCL位姿输出的影响机制。

图1. AMCL包sensor文件夹展开

sensor文件夹主要表达了两个模型,一个是运动模型,根据机器人运动类型,选择相应的运动模型,用来将粒子一起运动(平移,旋转),得到粒子样本的位姿;另一个是观测模型,根据选定的观测更新模型,对给定的位姿计算权重。

在amcl_sensor.h定义基类AMCLSensor 和基类AMCLSensorData,为两个派生类amcl_laser和amcl_odom提供统一接口。

1.传感器模型介绍

先来看AMCLSensor类,主要功能是根据运动模型更新粒子滤波器,根据观测模型初始化滤波器,根据观测模型更新粒子滤波器,仅定义运动模型里的位姿。从这里可以看到,运动模型和观测模型与粒子滤波器联系紧密。

// Base class for all AMCL sensors
class AMCLSensor
{
  // Default constructor
  public: AMCLSensor();
         
  // Default destructor
  public: virtual ~AMCLSensor();

  // Update the filter based on the action model.  Returns true if the filter
  // has been updated.
  public: virtual bool UpdateAction(pf_t *pf, AMCLSensorData *data);

  // Initialize the filter based on the sensor model.  Returns true if the
  // filter has been initialized.
  public: virtual bool InitSensor(pf_t *pf, AMCLSensorData *data);

  // Update the filter based on the sensor model.  Returns true if the
  // filter has been updated.
  public: virtual bool UpdateSensor(pf_t *pf, AMCLSensorData *data);

  // Flag is true if this is the action sensor
  public: bool is_action;

  // Action pose (action sensors only)
  public: pf_vector_t pose;
};

再来看AMCLSensorData类,主要功能是把AMCLSensor类实例化的对象放在AMCLSensorData类里:

// Base class for all AMCL sensor measurements
class AMCLSensorData
{
  // Pointer to sensor that generated the data
  public: AMCLSensor *sensor;
          virtual ~AMCLSensorData() {}

  // Data timestamp
  public: double time;
};

2. 运动模型

接下来浓墨重彩地介绍运动模型,我们就amcl_odom.h 和amcl_odom.cpp展开讲述。

值得一提的是,amcl_odom.h的头文件引用使用了pf文件夹下的pf_pdf.h。不过这也没什么可紧张的,下一讲我们会介绍pf文件夹。

#include "amcl_sensor.h"
#include "../pf/pf_pdf.h" 

选择运动模型,这一般是根据机器人的运动类型来决定,比如我的机器人是差分驱动,那么我一般是选择差分运动模型odom_model_diff,如果我的机器人是全向驱动,那么就选择全向运动模型odom_model_omni。

//里程计运动模型
typedef enum
{
  ODOM_MODEL_DIFF,
  ODOM_MODEL_OMNI,
  ODOM_MODEL_DIFF_CORRECTED,
  ODOM_MODEL_OMNI_CORRECTED
} odom_model_t;     

使用继承了AMCLSensorData基类的AMCLOdomData派生类定义运动模型的数据结构。这里定义里程计的位姿pose以及里程计位姿pose的微小改变。

//里程计数据结构
// Odometric sensor data
class AMCLOdomData : public AMCLSensorData
{
  // Odometric pose 里程计位姿
  public: pf_vector_t pose;
  
  //里程计位姿改变
  // Change in odometric pose
  public: pf_vector_t delta;
};

接着定义里程计运动模型,使用AMCLOdom类来表示,它继承了AMCLSensor基类。主要的功能是定义不同运动模型的输入参数,以及通过运动来更新粒子滤波器。

 // Odometric sensor model
class AMCLOdom : public AMCLSensor
{
  // Default constructor
  public: AMCLOdom();

  public: void SetModelDiff(double alpha1, 
                            double alpha2, 
                            double alpha3, 
                            double alpha4);

  public: void SetModelOmni(double alpha1, 
                            double alpha2, 
                            double alpha3, 
                            double alpha4,
                            double alpha5);

  public: void SetModel( odom_model_t type,
                         double alpha1,
                         double alpha2,
                         double alpha3,
                         double alpha4,
                         double alpha5 = 0 );

  // Update the filter based on the action model.  Returns true if the filter
  // has been updated.
  public: virtual bool UpdateAction(pf_t *pf, AMCLSensorData *data);

  // Current data timestamp
  private: double time;
  
  // Model type
  private: odom_model_t model_type;

  // Drift parameters
  private: double alpha1, alpha2, alpha3, alpha4, alpha5;
};

进入amcl_odom.cpp里,几个小函数用来处理角度:normalize函数和angle_diff函数。

//normalize函数
static double
normalize(double z)
{
  return atan2(sin(z),cos(z));//atan2 返回以弧度表示的y/x的反正切;返回方向角
}

//angle_diff函数
static double
angle_diff(double a, double b)
{
  double d1, d2;
  a = normalize(a);
  b = normalize(b);
  d1 = a-b;
  d2 = 2*M_PI - fabs(d1);
  if(d1 > 0)
    d2 *= -1.0;
  if(fabs(d1) < fabs(d2))
    return(d1);
  else
    return(d2);//注意这里要么返回d1要么d2;只是为了返回一个约束在0-2pi之间的角度
}

总览amcl_odom.cpp,这里需要设置运动模型,以及将运动模型实例化。关于运动模型的设置,使用UpdateAction函数,它的输入是粒子滤波器pf以及AMCLSensorData类型的data。

//通过运动更新粒子滤波器,其输入为粒子滤波器pf和AMCLSensorData提供的data
bool AMCLOdom::UpdateAction(pf_t *pf, AMCLSensorData *data) 
{
  //在此使用AMCLOdomData类 创建一个里程计数据模型 ndata :只有pose ,delta
  AMCLOdomData *ndata;
  //实例化,来源于传入的AMCLSensorData的AMCLOdomData
  ndata = (AMCLOdomData*) data;
  //创建新的粒子sample集
  pf_sample_set_t *set;
  //这里设定粒子sample集由传入的粒子滤波器的粒子sample集和它的当前粒子sample集
  set = pf->sets + pf->current_set;
  //这里定义旧的位姿,即X(t-1),使用简单的vector加减运算
  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;
    
    //避免两个位姿之间靠得太近
    // 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] + 
            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]);//使用angle_diff函数
    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);
    
    //设置里程计的运动噪声
    // 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)));
    //遍历粒子sample集,对粒子sample集里的每个sample对象的pose进行更新
    for (int i = 0; i < set->sample_count; i++)
    {
      //新建一个粒子sample,用于接收粒子sample集的sample对象
      pf_sample_t* sample = set->samples + i;
      
      //使用pf_ran_gaussian函数采样生成位姿的微小变化
      // Sample pose differences 使用pf_ran_gaussian()产生delta_*_hat
      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));
      //对粒子sample里的pose进行更新;得到新的sample
      // 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;//先产生 sample->pose.v[2]再计算得到上面两个
    }
  }
  break;
  }
};

要问这个粒子的更新算法原理是什么,答案就在《概率机器人》第5章 机器人运动。

图2.基于里程计信息的采样算法

总的来说,运动模型的作用就是更新粒子sample的位姿。

3.观测模型

好的,下面开始介绍观测模型,该观测模型具体在amcl_laser.h和amcl_laser.cpp实现。

值得一提的是,amcl_laser.h的头文件引用使用了map文件夹下的map.h。这是为什么呢?稍后解释。

#include "amcl_sensor.h"
#include "../map/map.h"

观测模型对应的测距仪是激光传感器,我们需要设置激光模型的相关参数:

// Laser model params
  //
  // Mixture params for the components of the model; must sum to 1
  private: double z_hit;//具有局部测量噪声的正常范围,对应高斯分布模型,满足正态分布
  private: double z_short;//意外对象引起,对应指数分布
  private: double z_max;//检测失败时,会返回最大值,满足点群分布
  private: double z_rand;//随机测量,即产生了无法解释的测量,满足分布在完整传感器测量范围的均匀分布
  //
  // Stddev of Gaussian model for laser hits. 高斯模型 
  private: double sigma_hit;
  // Decay rate of exponential model for short readings. 指数模型
  private: double lambda_short;
  // Threshold for outlier rejection (unused)
  private: double chi_outlier;

关于测距仪的传感器模型的组成,见图3所示,参考《概率机器人》第6章 机器人感知。

图3 测距仪传感器模型的组成:注意这里c)为点群分布,不是均匀分布!

选择观测模型,观测模型有好几种,有波束模型,似然域模型, 极大似然域模型。

typedef enum
{
  LASER_MODEL_BEAM,
  LASER_MODEL_LIKELIHOOD_FIELD,
  LASER_MODEL_LIKELIHOOD_FIELD_PROB
} laser_model_t;

使用AMCLLaserData类定义观测模型的数据结构,其中包含的基本成员是激光的测量值,最大测量距离,激光发出的射线数目。

// Laser sensor data
class AMCLLaserData : public AMCLSensorData
{
  public:
    AMCLLaserData () {ranges=NULL;};
    virtual ~AMCLLaserData() {delete [] ranges;};
  // Laser range data (range, bearing tuples)
  public: int range_count;
  public: double range_max;
  public: double (*ranges)[2];
};

接下来使用AMCLLaser类定义观测模型,先是定义了AMCLLaser的默认构造函数,它的输入为最大的激光射线发出的数目以及map_t格式的地图。

// Default constructor
AMCLLaser::AMCLLaser(size_t max_beams, map_t* map) : AMCLSensor(), 
						     max_samples(0), max_obs(0), 
						     temp_obs(NULL)
{
  this->time = 0.0;
  this->max_beams = max_beams;
  this->map = map;

  return;
}

值的注意的是 AMCLLaser类里包含了map_t格式的地图以及用于与里程计运动模型产生的位姿做转换的laser_pose。

  private: laser_model_t model_type;

  // Current data timestamp
  private: double time;

  // The laser map 注意 AMCLLaser类包含map,且是私有成员变量
  private: map_t *map;

  //注意 AMCLLaser类包含laser_pose,且是私有成员变量;用于与里程计模型产生的位姿做转换
  // Laser offset relative to robot 
  private: pf_vector_t laser_pose;
  
  // Max beams to consider
  private: int max_beams;

在AMCLLaser类里,我们可以设置不同的观测模型的参数,这里选择似然域模型并设置它的相关参数。注意这里还使用了map_update_cspace函数,更新cspace_distance.

void 
AMCLLaser::SetModelLikelihoodField(double z_hit,
                                   double z_rand,
                                   double sigma_hit,
                                   double max_occ_dist)
{
  this->model_type = LASER_MODEL_LIKELIHOOD_FIELD;
  this->z_hit = z_hit;
  this->z_rand = z_rand;
  this->sigma_hit = sigma_hit;
  map_update_cspace(this->map, max_occ_dist);//这里导入地图,更新cspace distance
}

我们需要根据观测模型的数据来更新滤波器,因此定义UpdateSensor函数设置激光观测模型。

// Apply the laser sensor model设置激光观测模型
bool AMCLLaser::UpdateSensor(pf_t *pf, AMCLSensorData *data) //输入为粒子滤波器pf,AMCLSensorData类型的data
{
  if (this->max_beams < 2)
    return false;

  // Apply the laser sensor model
  if(this->model_type == LASER_MODEL_BEAM)
    pf_update_sensor(pf, (pf_sensor_model_fn_t) BeamModel, data);
  else if(this->model_type == LASER_MODEL_LIKELIHOOD_FIELD)
    pf_update_sensor(pf, (pf_sensor_model_fn_t) LikelihoodFieldModel, data);  
  else if(this->model_type == LASER_MODEL_LIKELIHOOD_FIELD_PROB)
    pf_update_sensor(pf, (pf_sensor_model_fn_t) LikelihoodFieldModelProb, data);  
  else
    pf_update_sensor(pf, (pf_sensor_model_fn_t) BeamModel, data);

  return true;
}

使用SetLaserPose函数设置激光的位姿,使用选定的观测模型(这里选用LikelihoodFieldModel)来计算给定位姿的权重。

//似然域模型计算给定位姿的权重,其输入为粒子sample集set以及AMCLLaserData类型的的data
// Determine the probability for the given pose
double AMCLLaser::LikelihoodFieldModel(AMCLLaserData *data, pf_sample_set_t* set)
{
  AMCLLaser *self;
  int i, j, step;
  double z, pz;
  double p;
  double obs_range, obs_bearing;
  double total_weight;
  //创建粒子sample
  pf_sample_t *sample;
  //创建粒子sample的位姿
  pf_vector_t pose;
  pf_vector_t hit;

  self = (AMCLLaser*) data->sensor;

  total_weight = 0.0;

  //遍历传入的粒子sample集
  // Compute the sample weights
  for (j = 0; j < set->sample_count; j++)
  { 
    //这里接收到粒子sample的每个sample对象
    sample = set->samples + j;
    //存储接收的每个sample对象的位姿
    pose = sample->pose;
    
    //考虑激光的位姿与机器人的相对关系
    // Take account of the laser pose relative to the robot
    //从局部转换到全局;laser_pose(a)与里程计运动模型得出的位姿(b)的对应转换关系,最后得到位姿(c)
    pose = pf_vector_coord_add(self->laser_pose, pose);

    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;

    step = (data->range_count - 1) / (self->max_beams - 1);

    // Step size must be at least 1
    if(step < 1)
      step = 1;
    //遍历激光ranges范围内存储的所有距离值
    for (i = 0; i < data->range_count; i += step)
    {
      obs_range = data->ranges[i][0];
      obs_bearing = data->ranges[i][1];

      // This model ignores max range readings
      if(obs_range >= data->range_max)
        continue;

      // Check for NaN
      if(obs_range != obs_range)
        continue;

      pz = 0.0;
       
      //计算激光射线末端点的坐标
      // 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.转到map坐标系下的i,j
      int mi, mj;
      mi = MAP_GXWX(self->map, hit.v[0]);
      mj = MAP_GYWY(self->map, hit.v[1]);
      
      // 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
       //将map坐标系下的i,j转换成map坐标系下的index
        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;
    }
    //计算得到粒子sample的权重
    sample->weight *= p;
    total_weight += sample->weight;
  }
  //返回权重
  return(total_weight);
}

关于似然域模型,参考《概率机器人》第6章 机器人感知,算法如图4所示。

图4.利用与最近物体的欧式距离计算测距仪似然的算法

另外比较重要的rellocTempData函数,其根据粒子sample数目的最大值和max_obs进行重新分配。

//新的样本,新的max_obs
 void AMCLLaser::reallocTempData(int new_max_samples, int new_max_obs){ 
  if(temp_obs){
    for(int k=0; k < max_samples; k++){
      delete [] temp_obs[k];
    }
    delete []temp_obs; 
  }
  max_obs = new_max_obs; 
  max_samples = fmax(max_samples, new_max_samples); 

  temp_obs = new double*[max_samples]();
  for(int k=0; k < max_samples; k++){
    temp_obs[k] = new double[max_obs]();
  }

4.总结

从上面的介绍来看,里程计的运动模型对传入的粒子sample集的每个sample的位姿进行更新,激光传感器的观测模型对传入的粒子sample集的每个sample的位姿计算出权重。

因为是观测模型,所以需要知道机器人所在的位置,精确到栅格地图(栅格单元),才能计算出距离障碍物的距离。因此AMCLLaser类里定义了map_t格式的map,调用了map_update_cspace函数来更新cspace distance以及定义了激光测距仪的末端的位姿laser_pose。

使用pf_vector_coord_add 函数 将激光的位姿laser_pose转换到里程计运动模型产生的粒子sample的位姿所在的坐标系。这里就有运动模型与观测模型的关联了。

  /*来自amcl_laser.cpp

  */
// Take account of the laser pose relative to the robot
//从局部转换到全局;laser_pose(a)与这是里程计位姿(b)的对应转换关系,最后得到位姿(c)
pose = pf_vector_coord_add(self->laser_pose, pose);

关于pf_vector_coord_add函数:

/*来自pf_vector.cpp*/
// Transform from local to global coords (a + b)
pf_vector_t pf_vector_coord_add(pf_vector_t a, pf_vector_t b)
{
  pf_vector_t c;

  c.v[0] = b.v[0] + a.v[0] * cos(b.v[2]) - a.v[1] * sin(b.v[2]);
  c.v[1] = b.v[1] + a.v[0] * sin(b.v[2]) + a.v[1] * cos(b.v[2]);
  c.v[2] = b.v[2] + a.v[2];
  c.v[2] = atan2(sin(c.v[2]), cos(c.v[2]));
  
  return c;
}

还有,从里程计运动模型的采样算法来看,存在一个旧的位姿,这个旧的位姿如何产生呢?

// Apply the action model 设置运动模型 
bool AMCLOdom::UpdateAction(pf_t *pf, AMCLSensorData *data) //更新运动,其输入为粒子滤波器pf和AMCLSensorData提供的data
{
  AMCLOdomData *ndata;//在此使用AMCLOdomData类 创建一个里程计数据模型 ndata :只有pose ,delta
  ndata = (AMCLOdomData*) data;//实例化?来源于传入的AMCLSensorData的AMCLOdomData

  // Compute the new sample poses 创建新的sample集,其实是用于接收传入的pf里的粒子sample集
  pf_sample_set_t *set;
  set = pf->sets + pf->current_set;
  //这个是旧的位姿,使用简单的vector加减
  pf_vector_t old_pose = pf_vector_sub(ndata->pose, ndata->delta);

 

转自:4.AMCL包源码分析 | 传感器模型与sensor文件夹 - 知乎 (zhihu.com)

 

 

 

 

posted on 2022-09-14 13:53  tdyizhen1314  阅读(357)  评论(0编辑  收藏  举报

导航