• (笔记)(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)

     

     

     

     

  • 相关阅读:
    发送ajax步骤
    web app开发技巧总结
    移动端常见问题及解决方案
    【转】Session Cookie Token的区别
    call、apply、bind的区别
    【转】rem自适应布局
    ThinkPHP系统内置单字母函数函数
    thinkphp buildsql和fetchsql的区别
    thinkphp 3.2.3重写 pdo链接
    thinkphp pdo 重写问题
  • 原文地址:https://www.cnblogs.com/tdyizhen1314/p/16692794.html
Copyright © 2020-2023  润新知