Apollo感知模块具有识别障碍物交通灯的能力;

LiDAR利用点云感知,可以了解障碍物位置、大小、类别、朝向、轨迹和 速度等信息。

Apollo解决的障碍物感知问题有:

· 高精地图ROI过滤器(HDMap ROI Filter)

· 基于卷积神经网络分割(CNNSegmentation)

· MinBox障碍物边框构建(MinBoxBuilder)

· HM对象跟踪(HM Object Tracker)等

Apollo视觉感知输出的步骤(主要输出步骤

第一步是做detection(目标检测)和分割,

第二步是要把结果2D-to-3D,同时还有一个tracking(追踪)的过程

其中,tracking是做时序,2D和3D结果也会做tracking,这是无人车做感知常有的pipeline(线性模型),有这样的结果以后,就会输出被感知物体的位置和速度等信息。

在障碍物边框构建这一环节,开发者使用CNN Seg DL学习的方法,得到障碍物信息后,可使用MinBox的方法得到物体的2D框(最终边界框)结合tracking结合相机图像进行学习得到的物体边界框,则可得到物体3D的边界框。

以下是Apollo社区开发者朱炎亮在Github-Apollo-Note上分享的《如何构建MinBox障碍物边框》,感谢他为我们在MinBox Builder这一步所做的详细注解和释疑。

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

对象构建器组件为检测到的障碍物建立一个边界框

因为LiDAR传感器的遮挡或距离,形成障碍物的点云可以是稀疏的,并且仅覆盖一部分表面。

因此,盒构建器将恢复给定多边形点的完整边界框。即使点云稀疏,边界框的主要目的还是预估障碍物(例如,车辆)的方向。同样地,边框也用于可视化障碍物。

算法背后的想法是找到给定多边形点边缘所有区域

在以下示例中,如果AB是边缘,则Apollo将其他多边形点投影到AB上,并建立具有最大距离的交点对,这是属于边框的边缘之一。然后直接获得边界框的另一边。通过迭代多边形中的所有边,如下图所示,Apollo确定了一个6边界边框,将选择具有最小面积的方案作为最终的边界框

我们从代码入手,一步步解析障碍物边框构建的流程。

上一步CNN分割后期处理,可以得到LiDAR一定区域内的障碍物集群。接下来,我们将对这些障碍物集群建立其标定框。标定框的作用除了标识物体,还有一个作用就是标记障碍物的长length、宽width、高height

其中规定长length大于宽width,障碍物方向就是长的方向direction。

MinBox构建过程如下:

· 计算障碍物2D投影(高空鸟瞰XY平面)下的多边形polygon(如下图B)。

· 根据上述多边形,计算最适边框(如下图C)。

大致的代码框架如下:

   /// file in apollo/modules/perception/obstacle/onboard/lidar_process_subnode.ccvoid LidarProcessSubnode::OnPointCloud(const sensor_msgs::PointCloud2& message) {  /// call hdmap to get ROI...  /// call roi_filter...  /// call segmentor...  /// call object builderif (object_builder_ != nullptr) {ObjectBuilderOptions object_builder_options;   if (!object_builder_->Build(object_builder_options, &objects))
{...}}}/// file in apollo/modules/perception/obstacle/lidar/object_builder/min_box/min_box.ccbool MinBoxObjectBuilder::Build(const ObjectBuilderOptions& options, std::vector<ObjectPtr>* objects) {  for (size_t i = 0; i < objects->size(); ++i) {    if ((*objects)[i]) {      BuildObject(options, (*objects)[i]);}}}void MinBoxObjectBuilder::BuildObject(ObjectBuilderOptions options, ObjectPtr object)
{ ComputeGeometricFeature(options.ref_center, object);}void MinBoxObjectBuilder::ComputeGeometricFeature(const Eigen::Vector3d& ref_ct, ObjectPtr obj){
// step 1: compute 2D xy plane's polygen15    ComputePolygon2dxy(obj); // step 2: construct box16    ReconstructPolygon(ref_ct, obj);}

以上部分是MinBox障碍物边框构建的主题框架代码,

构建的两个过程分别在ComputePolygon2dxy 和 ReconstructPolygon函数完成,

下面我们就具体深入这两个函数,详细了解一下Apollo对障碍物构建的一个流程,和其中一些令人费解的代码段。

Step 1. MinBox构建--计算2DXY平面投影

这个阶段主要作用是障碍物集群做XY平面下的凸包多边形计算,最终得到这个多边形的一些角点。第一部分相对比较简单,没什么难点,计算凸包是调用plc库的ConvexHull组件(具体请参考pcl::ConvexHull)。

Apollo的凸包计算代码如下:

 1  /// file in apollo/modules/perception/obstacle/lidar/object_builder/min_box/min_box.ccvoid MinBoxObjectBuilder::ComputePolygon2dxy(ObjectPtr obj) {2    ...3    ConvexHull2DXY<pcl_util::Point> hull;4    hull.setInputCloud(pcd_xy);5    hull.setDimension(2);6    std::vector<pcl::Vertices> poly_vt;7    PointCloudPtr plane_hull(new PointCloud);8    hull.Reconstruct2dxy(plane_hull, &poly_vt);  if (poly_vt.size() == 1u) {9      std::vector<int> ind(poly_vt[0].vertices.begin(), poly_vt[0].vertices.end());    TransformPointCloud(plane_hull, ind, &obj->polygon);
10    } else {
11      ...
12    }
13  }/// file in apollo/modules/perception/common/convex_hullxy.htemplate <typename PointInT>class ConvexHull2DXY : public pcl::ConvexHull<PointInT> {public:14    void Reconstruct2dxy(PointCloudPtr hull, std::vector<pcl::Vertices> *polygons) {    PerformReconstruction2dxy(hull, polygons, true);
15    }  void PerformReconstruction2dxy(PointCloudPtr hull, std::vector<pcl::Vertices> *polygons, bool fill_polygon_data = false) {
16      coordT *points = reinterpret_cast<coordT *>(calloc(indices_->size() * dimension, sizeof(coordT)));    // step1. Build input data, using appropriate projection17      int j = 0;    for (size_t i = 0; i < indices_->size(); ++i, j += dimension) {
18        points[j + 0] = static_cast<coordT>(input_->points[(*indices_)[i]].x);
19        points[j + 1] = static_cast<coordT>(input_->points[(*indices_)[i]].y);
20      }    // step2. Compute convex hull21      int exitcode = qh_new_qhull(dimension, static_cast<int>(indices_->size()), points, ismalloc, const_cast<char *>(flags), outfile, errfile);
22      std::vector<std::pair<int, Eigen::Vector4f>, Eigen::aligned_allocator<std::pair<int, Eigen::Vector4f>>> idx_points(num_vertices);
23      FORALLvertices {
24        hull->points[i] = input_->points[(*indices_)[qh_pointid(vertex->point)]];
25        idx_points[i].first = qh_pointid(vertex->point);
26        ++i;
27      }    // step3. Sort28      Eigen::Vector4f centroid;    pcl::compute3DCentroid(*hull, centroid);    for (size_t j = 0; j < hull->points.size(); j++) {
29        idx_points[j].second[0] = hull->points[j].x - centroid[0];
30        idx_points[j].second[1] = hull->points[j].y - centroid[1];
31      }    std::sort(idx_points.begin(), idx_points.end(), pcl::comparePoints2D);
32      polygons->resize(1);
33      (*polygons)[0].vertices.resize(hull->points.size());    for (int j = 0; j < static_cast<int>(hull->points.size()); j++) {
34        hull->points[j] = input_->points[(*indices_)[idx_points[j].first]];
35        (*polygons)[0].vertices[j] = static_cast<unsigned int>(j);
36      }
37    }
38  }

从以上代码的注释中,我们可以清楚地了解到这个多边形顶点的求解流程,具体函数由PerformReconstruction2dxy函数完成,这个函数其实跟pcl库自带的很像pcl::ConvexHull::performReconstruction2D/Line76,其实Apollo开发人员几乎将pcl库的performReconstruction2D原封不动地搬了过来,仅去掉了一些冗余的信息。

这个过程主要有:

1. 构建输入数据,将输入的点云复制到coordT *points做处理。

2. 计算障碍物点云的凸包,得到的结果是多边形顶点。调用qh_new_qhull函数。

3. 顶点排序,从pcl::comparePoints2D/Line59

可以看到排序是角度越大越靠前,atan2函数的结果是[-pi,pi]。所以就相当于是顺 时针对顶点进行排序。

上图为计算多边形交点的流程示意图

该过程并不繁琐,这里不再深入解释每个模块。

Step 2. MinBox构建--边框构建

边框构建的逻辑,大致是针对过程中得到的多边形的每一条边。

将剩下的所有点都投影到这条边上,就可以计算边框Box的一条边长度(最远的两个投影点距离)

同时选择距离该条边最远的点计算Box的,这样就可以得到一个Box(上图case1-7分别是以这个多边形7条边作投影得到的7个Box),最终选择Box面积最小的边框作为障碍物的边框。

上图中case7得到的Box面积最小,所以case7中的Box就是最终障碍物的边框。当边框确定以后,就可以得到障碍物的长度length(大边长),宽度(小边长),方向(大边上对应的方向),高度(点云的平均高度,CNN分割与后期处理阶段得到)。

但是在此过程中,不得不承认有部分代码块相对难理解,而且出现了很多实际问题来优化这个过程。这里我将对这些问题一一进行解释。

根据代码,先简单地将这个过程归结为2步

1. 投影边长的选择(为什么要选择?因为背对lidar那一侧的点云是稀疏的那一侧的多边形顶点是不可靠的,不用来计算Box)。

2. 每个投影边长计算Box。

在进入正式的代码详解以前,这里有几个知识点需要了解。

假设向量a=(x0,y0),向量b=(x1,y1),那么有:

· 两个向量的点乘, a·b = x0x1 + y0y1\。

· 计算向量a在向量b上的投影: v = a·b/(b^2)·b,投影点的坐标就是v+(b.x, b.y)。

· 两个向量的叉乘, axb = |a|·|b|sin(theta) = x0y1 - x1y0,叉乘方向与ab平面垂直,遵循右手法则。叉乘模大小另一层意义是: ab向量构成的平行四边形面积。

如果两个向量a,b共起点,那么axb小于0,

那么a to b的逆时针夹角大于180度;等于则共线;大于0,a to b的逆时针方向夹角小于180度。

接下来正式解剖

ReconstructPolygon构建的代码:

(1) Step1:投影边长的选择

 1  /// file in apollo/modules/perception/obstacle/lidar/object_builder/min_box/min_box.ccvoid MinBoxObjectBuilder::ReconstructPolygon(const Eigen::Vector3d& ref_ct, ObjectPtr obj) {  // compute max_point and min_point2    size_t max_point_index = 0;  size_t min_point_index = 0;3    Eigen::Vector3d p;4    p[0] = obj->polygon.points[0].x;5    p[1] = obj->polygon.points[0].y;6    p[2] = obj->polygon.points[0].z;7    Eigen::Vector3d max_point = p - ref_ct;8    Eigen::Vector3d min_point = p - ref_ct;  for (size_t i = 1; i < obj->polygon.points.size(); ++i) {9      Eigen::Vector3d p;
10      p[0] = obj->polygon.points[i].x;
11      p[1] = obj->polygon.points[i].y;
12      p[2] = obj->polygon.points[i].z;
13      Eigen::Vector3d ray = p - ref_ct;    // clock direction14      if (max_point[0] * ray[1] - ray[0] * max_point[1] < EPSILON) {
15        max_point = ray;
16        max_point_index = i;
17      }    // unclock direction18      if (min_point[0] * ray[1] - ray[0] * min_point[1] > EPSILON) {
19        min_point = ray;
20        min_point_index = i;
21      }
22    }
23  }

以上代码内容为计算min_point和max_point两个角点,该角点到底是什么?其中关于EPSILON的比较条件到底代表什么

有一个前提我们已经在polygons多边形角点计算中可知:obj的polygon中所有角点都是顺时针按照arctan角度由大到小排序。

此过程可以通过下图了解其作用:

图中叉乘与0(EPSILON)的大小是根据前面提到的,两个向量的逆时针夹角。从上图中可以清晰地看到:max_pointmin_point就代表了LiDAR能检测到障碍物的两个极端点。

 1  /// file in apollo/modules/perception/obstacle/lidar/object_builder/min_box/min_box.ccvoid MinBoxObjectBuilder::ReconstructPolygon(const Eigen::Vector3d& ref_ct, ObjectPtr obj) {  // compute max_point and min_point2    ...  // compute valid edge3    Eigen::Vector3d line = max_point - min_point;  double total_len = 0;  double max_dis = 0;  bool has_out = false;  for (size_t i = min_point_index, count = 0; count < obj->polygon.points.size(); i = (i + 1) % obj->polygon.points.size(), ++count) {    //Eigen::Vector3d p_x = obj->polygon.point[i]4      size_t j = (i + 1) % obj->polygon.points.size();    if (j != min_point_index && j != max_point_index) {      // Eigen::Vector3d p = obj->polygon.points[j];5        Eigen::Vector3d ray = p - min_point;      if (line[0] * ray[1] - ray[0] * line[1] < EPSILON) {6          ...7        }else {8          ...9        }
10      } else if ((i == min_point_index && j == max_point_index) || (i == max_point_index && j == min_point_index)) {
11        ...
12      } else if (j == min_point_index || j == max_point_index) {      // Eigen::Vector3d p = obj->polygon.points[j];13        Eigen::Vector3d ray = p_x - min_point;      if (line[0] * ray[1] - ray[0] * line[1] < EPSILON) {
14          ...
15        } else {
16          ...
17        }
18      }
19    }
20  }

当计算得到max_point和min_point后就需要执行这段代码,这段代码可能令人费解的原因为——为什么需要对每条边做一个条件筛选?

请看下图:

上图中,A演示了这段代码对一个汽车的点云多边形进行处理,最后的处理结果可以看到只有Edge45、Edge56、Edge67是有效的,最终会被计入total_len和max_dist。并且,相对应的边都是在line(max_point-min_point)这条分界线的一侧,同时也是靠近LiDAR这一侧。

这说明靠近LiDAR这一侧点云检测效果好,边稳定;而背离LiDAR那一侧,会因为遮挡原因,往往很难(有时候不可能)得到真正的顶点,如上图B所示。

(2) Step2:投影边长Box计算

投影边长Box计算由ComputeAreaAlongOneEdge函数完成,下面我们分析这个函数的代码:

 1  /// file in apollo/modules/perception/obstacle/lidar/object_builder/min_box/min_box.ccdouble MinBoxObjectBuilder::ComputeAreaAlongOneEdge(2      ObjectPtr obj, size_t first_in_point, Eigen::Vector3d* center,    double* lenth, double* width, Eigen::Vector3d* dir) {  // first for3    std::vector<Eigen::Vector3d> ns;4    Eigen::Vector3d v(0.0, 0.0, 0.0);      // 记录以(first_in_point,first_in_point+1)两个定点为边,所有点投影,距离这条边最远的那个点5    Eigen::Vector3d vn(0.0, 0.0, 0.0);     // 最远的点在(first_in_point,first_in_point+1)这条边上的投影坐标6    Eigen::Vector3d n(0.0, 0.0, 0.0);      // 用于临时存储7    double len = 0;  double wid = 0;  size_t index = (first_in_point + 1) % obj->polygon.points.size();  for (size_t i = 0; i < obj->polygon.points.size(); ++i) {    if (i != first_in_point && i != index) {      // o = obj->polygon.points[i]8        // a = obj->polygon.points[first_in_point]9        // b = obj->polygon.points[first_in_point+1]10        // 计算向量ao在ab向量上的投影,根据公式:k = ao·ab/(ab^2), 计算投影点坐标,根据公式k·ab+(ab.x, ab.y)11        double k =  ((a[0] - o[0]) * (b[0] - a[0]) + (a[1] - o[1]) * (b[1] - a[1]));
12        k = k / ((b[0] - a[0]) * (b[0] - a[0]) + (b[1] - a[1]) * (b[1] - a[1]));
13        k = k * -1;
14        n[0] = (b[0] - a[0]) * k + a[0];
15        n[1] = (b[1] - a[1]) * k + a[1];
16        n[2] = 0;      // 计算由ab作为边,o作为顶点的平行四边形的面积,利用公式|ao x ab|,叉乘的模就是四边形的面积,17        Eigen::Vector3d edge1 = o - b;
18        Eigen::Vector3d edge2 = a - b;      double height = fabs(edge1[0] * edge2[1] - edge2[0] * edge1[1]);      // 利用公式: 面积/length(ab)就是ab边上的高,即o到ab边的垂直距离, 记录最大的高19        height = height / sqrt(edge2[0] * edge2[0] + edge2[1] * edge2[1]);      if (height > wid) {
20          wid = height;
21          v = o;
22          vn = n;
23        }
24      } else {
25        ...
26      }
27      ns.push_back(n);
28    }
29  }

从以上部分代码可以看出,ComputeAreaAlongOneEdge函数接受的输入包括多边形顶点集合,起始边first_in_point。代码将以first_in_point和first_in_point+1两个顶点构建一条边,将集合中其他点都投影到这条边上,并计算顶点距离这条边的高——也就是垂直距离。

最终的结果会保存到ns中。代码中,k的计算利用了两个向量点乘来计算投影点的性质;height的计算利用了两个向量叉乘的模等于两个向量组成的四边形面积的性质。

1  /// file in apollo/modules/perception/obstacle/lidar/object_builder/min_box/min_box.ccdouble MinBoxObjectBuilder::ComputeAreaAlongOneEdge(  // first for2    ...  // second for3    size_t point_num1 = 0;  size_t point_num2 = 0;  // 遍历first_in_point和first_in_point+1两个点以外的,其他点的投影高,选择height最大的点,来一起组成Box4    // 这两个for循环是寻找ab边上相聚最远的投影点,因为要把所有点都包括到Box中,所以Box沿着ab边的边长就是最远两个点的距离,可以参考边框构建。5    for (size_t i = 0; i < ns.size() - 1; ++i) {6      Eigen::Vector3d p1 = ns[i];    for (size_t j = i + 1; j < ns.size(); ++j) {7        Eigen::Vector3d p2 = ns[j];      double dist = sqrt((p1[0] - p2[0]) * (p1[0] - p2[0]) + (p1[1] - p2[1]) * (p1[1] - p2[1]));      if (dist > len) {8          len = dist;9          point_num1 = i;
10          point_num2 = j;
11        }
12      }
13    }  // vp1和vp2代表了Box的ab边对边的那条边的两个顶点,分别在v的两侧,方向和ab方向一致。14    Eigen::Vector3d vp1 = v + ns[point_num1] - vn;
15    Eigen::Vector3d vp2 = v + ns[point_num2] - vn;  // 计算中心点和面积16    (*center) = (vp1 + vp2 + ns[point_num1] + ns[point_num2]) / 4;
17    (*center)[2] = obj->polygon.points[0].z;  if (len > wid) {
18      *dir = ns[point_num2] - ns[point_num1];
19    } else {
20      *dir = vp1 - ns[point_num1];
21    }
22    *lenth = len > wid ? len : wid;
23    *width = len > wid ? wid : len;  return (*lenth) * (*width);
24  }

剩下的代码就是计算Box的四个顶点坐标,以及它的面积Area。

综上所述,Box经过上述(1)(2)两个阶段,可以很清晰地得到每条有效边(靠近lidar一侧,在min_point和max_point之间)对应的Box四个顶点坐标、宽、高。最终选择Box面积最小的作为障碍物预测Box。

整个过程的代码部分相对较难理解,经过本节的学习,相信各位应该对MinBox边框的构建有了一定了解。

希望对你有帮助。

Apollo感知解析之MinBox障碍物边框构建相关推荐

  1. 「Apollo」Apollo感知汇总

    参考链接:3D obstacle Perception 1 感知Perception概览 整个apollo perception传感器架构图如下: 整个感知模块的硬件方面包括了多个相机.毫米波雷达(前 ...

  2. 「Apollo」百度Apollo感知模块(perception)红绿灯检测代码完整+详细解析

    1 背景 最近在读apollo感知模块下的红绿灯检测,apollo框架思路清晰,风格规范,值得多读.直接上代码文件:trafficlights_perception_component.cc traf ...

  3. Apollo进阶课程⑲丨Apollo感知之旅——感知算法

    目录 点云感知 启发式方法:NCut 深度学习方法:CNNSeg 视觉感知 CNN检测 CNN分割 后处理 红绿灯感知 基于深度学习的红绿灯感知模块 Radar感知 超声波感知 原文链接:进阶课程⑲丨 ...

  4. Apollo自动驾驶进阶课(5)——Apollo感知技术

    1.感知 机器感知系统根据各种各样的传感器来获取汽车周围的驾驶环境,包括Lidar,Camera,Radar,超声波雷达以及拾音器等.相对人类而言,机器感知是全覆盖,并且感知精度更高,能够达到厘米级别 ...

  5. Apollo星火计划学习笔记第六讲——Apollo感知模块详解实践2感知基础

    Apollo学习笔记 零.目录 一.Apollo感知框架介绍 1.1 检测和分类 1.2 跟踪 1.3 感知模块代码结构 1.3.1 感知模块入口 二.Lidar障碍物检测.红绿灯识别 2.1 Lid ...

  6. Apollo进阶课程⑳丨Apollo感知之旅——机器学习与感知的未来

    目录 1机器学习 可解释性是否需要 其它算法 2感知的未来 Sensor迭代 深度学习+仿真数据+AI芯片 智能交通设施 3思考 原文链接:进阶课程⑳丨Apollo感知之旅--机器学习与感知的未来 自 ...

  7. Apollo进阶课程⑰丨Apollo感知之旅——传感器选择和安装

    目录 1.激光雷达 2.相机 3.Radar毫米波 4.安装传感器 原文链接:进阶课程⑰丨Apollo感知之旅--传感器选择和安装 上周阿波君为大家详细介绍了「进阶课程⑯ Apollo感知之旅--感知 ...

  8. Apollo进阶课程⑯丨Apollo感知之旅——感知概貌

    原文链接:进阶课程⑯丨Apollo感知之旅--感知概貌 上周阿波君为大家详细介绍了「进阶课程⑮| Apollo无人车自定位技术入门」. 我们人类天生就配备多种传感器,眼睛可以看到周围的环境,耳朵可以用 ...

  9. Baidu Apollo代码解析之EM Planner中的QP Speed Optimizer 1

    大家好,我已经把CSDN上的博客迁移到了知乎上,欢迎大家在知乎关注我的专栏慢慢悠悠小马车(https://zhuanlan.zhihu.com/duangduangduang).希望大家可以多多交流, ...

  10. 无人驾驶算法——Baidu Apollo代码解析之ReferenceLine Smoother参考线平滑

    无人驾驶算法--Baidu Apollo代码解析之ReferenceLine Smoother参考线平滑 Apollo 参考线平滑类 reference_line_provider.cc 代价函数 c ...

最新文章

  1. lodash(一)数组
  2. JScrollPane 滚动处理
  3. 数据挖掘 —— 探索性数据分析
  4. 【论文解读】Confluence:物体检测中不依赖IoU的NMS替代算法论文解析
  5. java stopself_然后,即使我停止了服务,Context.startForegroundService()也没有调用Service.startForeground()...
  6. Netty : writeAndFlush的线程安全及并发问题
  7. python免费教程视频-微软推出 Python 免费在线教程视频
  8. 手机页面图片显示高低不一致
  9. fedro1 16 64位安装oracle 11.2.0.1遇到的问题
  10. 关于makefile的使用
  11. 华为发布了其自研的鸿蒙操作系统,官宣!鸿蒙手机操作系统即将发布
  12. mysql2008安装失败_sql server 2008为什么会安装失败 sql2008安装失败解决办法
  13. 亲密关系科学(03)夫妻相处的智慧
  14. Java项目中利用钉钉机器人Webhook向钉钉群推送告警通知
  15. 4、关于step的设置
  16. 这个世界有病,我们都有病
  17. Laravel 5.8 前瞻 1
  18. linux 【CPU性能】 系统中的软中断使用率升高怎么办?
  19. 维克样品管理软件 v1.2 免费
  20. 教资面试合格率是怎么算的?

热门文章

  1. ONNX转ms后,benchmark跑分非常慢
  2. 华为ME909之4G模块的开发
  3. 谈谈写博客的意义和感受
  4. 网站备案 服务器变更,网站变更服务器备案
  5. Hadoop资源管理
  6. 浅谈企业信息化的过程与阶段
  7. 卡通渲染 类罪恶装备与崩坏3渲染总结
  8. java graphics类详解_java 基础二 Graphics类
  9. 吴恩达机器学习打卡day6
  10. 工业产品常用的长度单位有哪些?