欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Apollo 感知分析之跟踪对象信息融合

程序员文章站 2022-07-12 09:47:29
...

本文章来自: Apollo开发者社区     原创:阿波君       

 

自动驾驶使用的感知类的传感器,主要有激光雷达、毫米波雷达、摄像头、组合导航。

激光雷达   安装在车顶,360度同轴旋转,可以提供周围一圈的点云信息。另外,激光雷达不光用于感知,也可用在定位高精度地图的测绘。

毫米波雷达 安装在保险杠上,与激光雷达原理类似,通过观察电磁波回波入射波的差异来计算速度距离

组合导航  分为两部分,一部分是GNSS板卡,另一部分是INS。当车辆行驶到林荫路或是建筑物附近,GPS会产生偏移或是信号屏蔽的情况,这时可通过与INS进行组合运算解决问题。GNSS用于定位,INS用于惯导)

而在Apollo多传感器融合定位模块的融合框架中,包括两部分:惯性导航解算Kalman滤波

融合定位的结果会反过来用于GNSS定位和点云定位的预测;

融合定位的输出是一个6-dof位置姿态,以及协方差矩阵。

自动驾驶中感知学习最大问题是系统对模块的要求,对准确率/召回率/响应延时等,要求很高,牵扯到安全。如果在自动驾驶中的感知学习中,出现一些障碍物的漏检、误检,就会会带来安全问题。漏检会带来碰撞,影响事故;误检会造成一些急刹,带来乘车体验的问题。

 

那么,在在进行几何计算+时序计算后,如何进行环视融合?

以下是Apollo社区开发者朱炎亮在Github-Apollo-Note上分享的《跟踪对象信息融合》,感谢他为我们在融合这一步所做的详细注解和释疑。

 

面对复杂多变、快速迭代的开发环境,只有开放才会带来进步,Apollo社区正在被开源的力量唤醒。

 

Apollo 感知分析之跟踪对象信息融合

 

在上一步HM对象跟踪步骤中,Apollo对每个时刻检测到的Object与跟踪列表中的TrackedObject进行匈牙利算法的二分图匹配,匹配结果分三类:

  1. 如果成功匹配,那么使用卡尔曼滤波器更新信息(重心位置、速度、加速度)等信息;

  2. 如果失配,缺少对应的TrackedObject,将Object封装成TrackedObject,加入跟踪列表

  3. 对于跟踪列表中当前时刻目标缺失的TrackedObject(Object都无法与之匹配),使用上时刻速度,跟新当前时刻的重心位置与加速度等信息(无法使用卡尔曼滤波更新,缺少观测状态)。对于那些时间过长的丢失的跟踪目标,将他们从跟踪队列中删除。

 

而在跟踪对象信息融合的阶段,Apollo的主要工作是,给定一个被跟踪的物体序列

(Time_1,TrackedObject_1),

(Time_2,TrackedObject_2),

...,

(Time_n,TrackedObject_n)

每次执行LidarSubNode的回调,都会刷新一遍跟踪列表,那么一个被跟踪物体的信息也将会被刷新(重心位置,速度,加速度,CNN物体分割--前景概率,CNN物体分割--各类别概率)。

N次调用都会得到N个概率分布(N个CNN物体分割--前景概率score,N个CNN物体分割--各类物体概率Probs),我们需要在每次回调的过程中确定物体的类别属性,当然最简单的方法肯定是argmax(Probs)。但是CNN分割可能会有噪声,所以最好的办法是将N次结果联合起来进行判断!

跟踪物体的属性可以分为4类:

  • UNKNOWN--未知物体

  • PEDESTRIAN--行人

  • BICYCLE--自行车辆

  • VEHICLE--汽车车辆

E.g. 5次跟踪的结果显示某物体的类别/Probs分别为:

跟踪时间戳 UNKNOWN概率 PEDESTRIAN概率 BICYCLE概率 VEHICLE概率 Argmax结果
frame1 0.1 0.2 0.6 0.1 BICYCLE
frame2 0.1 0.1 0.7 0.1 BICYCLE
frame3 0.1 0.2 0.2 0.5 VEHICLE
frame4 0.1 0.2 0.6 0.1 BICYCLE
frame5 0.1 0.2 0.6 0.1 BICYCLE

如果直接对每次跟踪使用argmax(Probs)直接得到结果,有时候会有误差,上表frame3的时候因为误差结果被认为是汽车,所以需要根据前面的N次跟踪结果一起联合确定物体类别属性。Apollo使用维特比算法求解隐状态概率。这里做一个简单地描述,具体参考维特比Viterbi算法:

维特比算法前提状态链是马尔可夫链即下一时刻的状态 仅仅取决于 当前状态。(假设隐状态数量为m,观测状态数量为n),隐状态分别为s1,s2,s3,....sm; 可观测状态分别为o1,o2,...,on。 则有:

状态转移矩阵P(mxm): P[i,j]代表状态i到状态j转移的概率。$\sum_{j=0}^m P[i,j] = 1$ 发射概率矩阵R(mxn): R[i,j]代表隐状态i能被观测到为j现象的概率。 $\sum_{j=0}^n P[i,j] = 1$

现在假设初始时刻的m个隐状态概率为:(s0_1, s0_2, ..., s0_m),第一时刻观测到的可观察状态为ok,那么如何求第一时刻的隐状态:

1、上时刻隐状态si,第一时刻隐状态sj的联合概率为:

$$ p(s1_j, s0_i) = p(prv_state=s0_i) * P(sj|si) $$

因此可以得到p(s1_1, s0_i), p(s1_2, s0_i), ..., p(s1_m, s0_i),也可以得到p(s1_j, s0_1), p(s1_j, s0_2), p(s1_j, s0_3),..., p(s1_j, s0_m)。最终是一个mxm的联合概率矩阵。

 

2、同时上时刻隐状态si,第一时刻隐状态sj情况下可观测到观察状态ok的概率为:

$$ p(ok|s1_j, s0_i) = p(s1_j, s0_i) * R(ok|sj) $$

同理可得到p(ok|s1_0, s0_i), p(ok|s1_1, s0_i), p(ok|s1_2, s0_i),..., p(ok|s1_m, s0_i),也是一个mxm的条件概率矩阵。

若最终的条件概率矩阵中p(ok|s1_jj, s0_ii) 值最大,那么就可以得出结论这个时刻的隐状态为s_jj。

Apollo也采取类似的Viterbi算法做隐状态的修正。

/// file in apollo/modules/perception/obstacle/onboard/lidar_process_subnode.cc
void LidarProcessSubnode::OnPointCloud(const sensor_msgs::PointCloud2& message) {
  /// call hdmap to get ROI
  ...
  /// call roi_filter
  ...
  /// call segmentor
  ...
  /// call object builder
  ...
  /// call tracker
  ...
  /// call type fuser
  if (type_fuser_ != nullptr) {
    TypeFuserOptions type_fuser_options;
    type_fuser_options.timestamp = timestamp_;
    if (!type_fuser_->FuseType(type_fuser_options, &(out_sensor_objects->objects))) {
      ...
    }
  }
}

/// file in apollo/modules/perception/common/sequence_type_fuser/sequence_type_fuser.cc
bool SequenceTypeFuser::FuseType(
    const TypeFuserOptions& options,
    std::vector<std::shared_ptr>* objects) {
  if (options.timestamp > 0.0) {
    sequence_.AddTrackedFrameObjects(*objects, options.timestamp);  // step1. add current TrackedObject into sequence
    std::map<int64_t, std::shared_ptr> tracked_objects;
    for (auto& object : *objects) {
      if (object->is_background) {                // step2. check whether is background
        object->type_probs.assign(static_cast<int>(ObjectType::MAX_OBJECT_TYPE), 0);
        object->type = ObjectType::UNKNOWN_UNMOVABLE;
        continue;
      }
      const int& track_id = object->track_id;
      // step3. crop sequence to generate a new sequence which length smaller or equal than temporal_window_(20)
      sequence_.GetTrackInTemporalWindow(track_id, &tracked_objects, temporal_window_);
      // step4. CRF to rectify object type use Viterbi algorithm
      if (!FuseWithCCRF(&tracked_objects)) {
        ...
      }
    }
  }
}


 

将TrackedObject加入到sequence(sequence数据结构为map<int, map从上述代码可以看到使用CRF进行隐状态(物体类别)修正,主要步骤为:

  1. 筛选,对于背景物体直接标记为UNKNOWN::UNMOVABLE类别

  2. 新序列生成,sequence[track_id]里面的已存储的记录过多,而修正只需要最近一段时间的序列即可,所以使用GetTrackInTemporalWindow函数可以获取近期temporal_window_((默认20)以内的所有物体跟踪序列

  3. CRF隐状态矫正,使用上述new_sequence配合Viterbi算法进行矫正

上述的难点就在于Step 4,因此本节从代码分析描述CRF修正的原理。在FuseWithCCRF函数*分两步

  • Step1. 状态平滑(物体状态整流)

  • Step2. Viterbi算法推离状态

 

/// file in apollo/modules/perception/common/sequence_type_fuser/sequence_type_fuser.cc
bool SequenceTypeFuser::FuseWithCCRF(std::map<int64_t, std::shared_ptr>* tracked_objects) {
  /// Step1. rectify object type with smooth matrices
  fused_oneshot_probs_.resize(tracked_objects->size());
  std::size_t i = 0;
  for (auto& pair : *tracked_objects) {
    std::shared_ptr& object = pair.second;
    if (!RectifyObjectType(object, &fused_oneshot_probs_[i++])) {
      AERROR << "Failed to fuse one shot probs in sequence.";
      return false;
    }
  }
}

bool SequenceTypeFuser::RectifyObjectType(const std::shared_ptr& object, Vectord* log_prob) {
  Vectord single_prob;
  fuser_util::FromStdVector(object->type_probs, &single_prob);
  auto iter = smooth_matrices_.find("CNNSegClassifier");

  static const Vectord epsilon = Vectord::Ones() * 1e-6;
  single_prob = iter->second * single_prob + epsilon;
  fuser_util::Normalize(&single_prob);

  double conf = object->score;
  single_prob = conf * single_prob + (1.0 - conf) * confidence_smooth_matrix_ * single_prob;
  fuser_util::ToLog(&single_prob);
  *log_prob += single_prob;
  return true;
}

原始CNN分割与后处理得到当前跟踪物体对应4类的概率为object->type_probs从上述代码可以得到以下结论:

  1. 原始CNN分割与后处理得到当前跟踪物体前景概率为conf=object->score,那么背景的概率为1-conf

  2. 平滑公式为:

    single_prob = iter->second * single_prob + epsilon single_prob = conf * single_prob + (1.0 - conf) * confidence_smooth_matrix_ * single_prob

 

iter->second(CNNSegClassifier Matrix)矩阵为:

0.9095 0.0238 0.0190 0.0476

0.3673 0.5672 0.0642 0.0014

0.1314 0.0078 0.7627 0.0980

0.3383 0.0017 0.0091 0.6508

 

confidence_smooth_matrix_(Confidence)矩阵为:

1.00 0.00 0.00 0.00

0.40 0.60 0.00 0.00

0.40 0.00 0.60 0.00

0.50 0.00 0.00 0.50

/// file in apollo/modules/perception/common/sequence_type_fuser/sequence_type_fuser.cc
bool SequenceTypeFuser::FuseWithCCRF(std::map<int64_t, std::shared_ptr>* tracked_objects) {
  /// rectify object type with smooth matrices
  ...
  /// use Viterbi algorithm to infer the state
  std::size_t length = tracked_objects->size();
  fused_sequence_probs_.resize(length);
  state_back_trace_.resize(length);
  fused_sequence_probs_[0] = fused_oneshot_probs_[0];
  /// add prior knowledge to suppress the sudden-appeared object types.
  fused_sequence_probs_[0] += transition_matrix_.row(0).transpose();
  for (std::size_t i = 1; i < length; ++i) {
    for (std::size_t right = 0; right < VALID_OBJECT_TYPE; ++right) {
      double max_prob = -DBL_MAX;
      std::size_t id = 0;
      for (std::size_t left = 0; left < VALID_OBJECT_TYPE; ++left) {
        const double prob = fused_sequence_probs_[i - 1](left) +
                            transition_matrix_(left, right) * s_alpha_ +
                            fused_oneshot_probs_[i](right);
        if (prob > max_prob) {
          max_prob = prob;
          id = left;
        }
      }
      fused_sequence_probs_[i](right) = max_prob;
      state_back_trace_[i](right) = id;
    }
  }
  std::shared_ptrobject = tracked_objects->rbegin()->second;
  RecoverFromLogProb(&fused_sequence_probs_.back(), &object->type_probs, &object->type);
  return true;
}

 

Viterbi算法推理代码如下:

那么根据上时刻的真实的隐状态fused_sequence_probs_[i-1]和状态转移矩阵transition_matrix_,可以求解开始提到的mxm(4x4)联合状态矩阵,其中矩阵中元素fused_sequence_probs_[i][j]的求解方式为:上述代码中,fused_oneshot_probs_是每个时刻独立的4类概率,经过平滑和log(·)处理。transition_matrix_是状态转移矩阵P,维度为4x4,经过log(·)处理,fused_sequence_probs_为Viterbi算法推理后的修正状态(也就是真实的隐状态)。

p(si_j, si-1_k) = p(prv_state=si-1_k) * P(sj|sk)

对应代码:

const double prob = fused_sequence_probs_[i - 1](left) +
                    transition_matrix_(left, right) * s_alpha_ +
                    fused_oneshot_probs_[i](right);

 

上述存在几个问题:

  • 问题1: 为什么代码用的加法?

因为代码中是将乘法转换到log(·)做运算,上述已提到transition_matrix_和fused_oneshot_probs_都是经过log(·)处理。那么上述公式等价于:

log p(si_j, si-1_k) = log p(prv_state=si-1_k) + log P(sj|sk)

只要最终将log p(si_j, si-1_k) 经过 exp(·)处理还原真实的概率即可。

 

  • 问题2. s_alpha_是什么意思?

有待后续深入研究,这里还不曾研究透。

 

  • 问题3. 为什么代码会额外多乘一个概率

p(ok|sj) -- used_oneshot_probs_[i](right)

Apollo中对于当前状态的推理就是,求解前后两个时刻关联最紧密(上时刻各状态中与当前时刻状态sj变换概率最大的状态si),本质就是求解开始例子中提到联合概率矩阵。在开始的例子中,我们举例隐状态s共m个。紧接着计算前后两个时刻状态的联合概率矩阵。在这个例子中,隐状态是onehot类型,也就是[0,0,0,si-1=1,0,0,0],那么当我们的隐状态不是onehot,也就是这种类型[0.1,0.1,0.1,si-1=0.6,0,0,0.1],那么如何求解联合概率矩阵?

做法也很类似,只需要将原先计算目标:

p(si_j, si-1_k) = p(prv_state=sk) * P(sj|si)

变成计算目标:

p_new(si_j, si-1_k) = p(prv_state=sk) * P(sj|si) * p(current_state=sj)

上述代码中const double prob就是联合概率矩阵:p_new(si_j, si-1_k),最终取最大元素对应的(ii,jj)组,也就是(left,right)组,ii(left)就是当前时刻的隐状态:

for (std::size_t right = 0; right < VALID_OBJECT_TYPE; ++right) {  // time sequence loop
  double max_prob = -DBL_MAX;
  std::size_t id = 0;
  for (std::size_t left = 0; left < VALID_OBJECT_TYPE; ++left) {  // previous state loop
    const double prob = fused_sequence_probs_[i - 1](left) +     // each elem p[i,j] in union probability matrix
                        transition_matrix_(left, right) * s_alpha_ +
                        fused_oneshot_probs_[i](right);
    if (prob > max_prob) {   // find the previous state and current hidden state which have maximum value 
      max_prob = prob;
      id = left;
    }
  }
  fused_sequence_probs_[i](right) = max_prob; // update 2 state connected intersity
  state_back_trace_[i](right) = id; // 2 frame's connection
}

从上述代码和注释,可以很明显的看到求解的过程,fused_sequence_probs_是各个时刻物体的真实修正状态,state_back_trace_是一条由后往前连接的最佳回溯状态链,如下图:

 

Apollo状态转移矩阵为:

0.34 0.22 0.33 0.11

0.03 0.90 0.05 0.02

0.03 0.05 0.90 0.02

0.06 0.01 0.03 0.90

经过上述计算得到的fused_sequence_probs_矩阵就是所有时刻跟踪物体的状态概率(log(·)处理过,所以必须经过exp(·)还原概率),最后就是求解当前时刻,也就是sequence最后一列各类物体的概率。

// get current time tracked object, last elem in sequence
std::shared_ptrobject = tracked_objects->rbegin()->second; 
// post-process
RecoverFromLogProb(&fused_sequence_probs_.back(), &object->type_probs, &object->type);

bool SequenceTypeFuser::RecoverFromLogProb(Vectord* prob,
                                           std::vector<float>* dst,
                                           ObjectType* type) {
  fuser_util::ToExp(prob);     // probability log(·) format to origin format by using exp(·) 
  fuser_util::Normalize(prob);  
  fuser_util::FromEigenVector(*prob, dst);  // assign probability from origin 6 classes to 4 classes
  *type = static_cast(std::distance(dst->begin(), std::max_element(dst->begin(), dst->end()))); // type with max prob
  return true;
}

void FromEigenVector(const Vectord& src_prob, std::vector<float>* dst_prob) {
  dst_prob->assign(static_cast<int>(ObjectType::MAX_OBJECT_TYPE), 0);
  dst_prob->at(0) = src_prob(0);
  for (std::size_t i = 3; i < static_cast<int>(ObjectType::MAX_OBJECT_TYPE); ++i) {
    dst_prob->at(i) = static_cast<float>(src_prob(i - 2));
  }
}

 

上面代码之所以有FromEigenVector过程,主要是Apollo原始定义了6中object type:

  • UNKNOWN--未知物体

  • UNKNOWN_MOVABLE--未知可移动物体

  • UNKNOWN_UNMOVABLE--未知不可移动物体

  • PEDESTRIAN--行人

  • BICYCLE--自行车辆

  • VEHICLE--汽车车辆

实际只用到了0,3,4,5四类。

 

希望对你有帮助。