之前写过一篇杂文《自动驾驶平台Apollo 2.5阅读手记:perception模块之camera detector》,介绍到用于camera输入的DNN模型不仅会输出物体检测结果,还会输出车道线的语义分割结果。但要得到最终的车道线信息,还要经过后处理。项目中自带一个例子cc_lane_post_processor_test,我们就以这个例子为线索,看下它的大体实现。
首先按《自动驾驶平台Apollo 2.5环境搭建》中介绍的进入到Apollo的docker运行环境中,编译完后,执行以下命令就可以跑了:
# 加GLOG_v=4是为了输出更多信息便于分析
GLOG_v=4 ./bazel-bin/modules/perception/obstacle/camera/lane_post_process/cc_lane_post_processor/cc_lane_post_processor_test
可能会碰到下面错误:The width of input lane map does not match。这是因为modules/perception/data/cc_lane_post_processor_test目录中的lane_map.jpg是全尺寸的,而配置文件lane_post_process_config.pb.txt中指定的是缩放后的ROI区域,大小为960/384。所以只要将lane_map.jpg换成yolo_camera_detector_test输出的这个:modules/perception/data/yolo_camera_detector_test/lane_map.jpg就行。如果要将结果画在原尺寸原图上,测试程序中也要做相应修改。另外改了一些参数,包括 1) 坐标空间类型,因为这里我们的输入是图片。2) 对lane map的阀值,默认的阀值太高,一被过滤就不剩什么了。3) 默认指定的模型目录是空的,所以换了个目录。具体修改如下:
diff --git a/modules/perception/model/camera/lane_post_process_config.pb.txt b/modules/perception/model/camera/lane_post_process_config.pb.txt
index c958661..8712672 100644
--- a/modules/perception/model/camera/lane_post_process_config.pb.txt
+++ b/modules/perception/model/camera/lane_post_process_config.pb.txt
@@ -1,6 +1,6 @@
name: "CCLanePostProcessor"
version: "0.1.0"
-space_type: "vehicle"
+space_type: "image"
image_width: 1920
image_height: 1080
roi: 0
@@ -14,7 +14,7 @@ non_mask: 1260
non_mask: 1079
non_mask: 660
non_mask: 1079
-lane_map_confidence_thresh: 0.97
+lane_map_confidence_thresh: 0.5
cc_split_siz: 50.0
cc_split_len: 15
min_cc_pixel_num: 50
diff --git a/modules/perception/model/yolo_camera_detector/lane.pt b/modules/perception/model/yolo_camera_detector/lane.pt
index 6ed2033..a15a1a3 100644
--- a/modules/perception/model/yolo_camera_detector/lane.pt
+++ b/modules/perception/model/yolo_camera_detector/lane.pt
@@ -1,5 +1,5 @@
model_param {
- model_name: "lane2d_0612"
+ model_name: "lane2d_0627"
proto_file: "deploy_lane_high.pt"
weight_file: "deploy_lane_high.md"
offset_ratio: 0.28843
diff --git a/modules/perception/obstacle/camera/lane_post_process/cc_lane_post_processor/cc_lane_post_processor_test.cc b/modules/perception/obstacle/camera/lane_post_process/cc_lane_post_processor/cc_lane_post_processor_test.cc
index 880d478..88292c2 100644
--- a/modules/perception/obstacle/camera/lane_post_process/cc_lane_post_processor/cc_lane_post_processor_test.cc
+++ b/modules/perception/obstacle/camera/lane_post_process/cc_lane_post_processor/cc_lane_post_processor_test.cc
@@ -96,12 +96,14 @@ TEST_F(CCLanePostProcessorTest, test_lane_post_process_vis) {
for (int h = 0; h < lane_map.rows; ++h) {
for (int w = 0; w < lane_map.cols; ++w) {
if (lane_map.at<float>(h, w) >= lane_map_conf_thresh) {
- vis_lane_objects_image.at<cv::Vec3b>(h, w)[0] =
- static_cast<unsigned char>(lane_mask_color[0]);
- vis_lane_objects_image.at<cv::Vec3b>(h, w)[1] =
- static_cast<unsigned char>(lane_mask_color[1]);
- vis_lane_objects_image.at<cv::Vec3b>(h, w)[2] =
- static_cast<unsigned char>(lane_mask_color[2]);
+ int orig_h = h * 2 + config_.start_y_pos();
+ int orig_w = w * 2;
+ vis_lane_objects_image.at<cv::Vec3b>(orig_h, orig_w)[0] =
+ static_cast<unsigned char>(lane_mask_color[0]);
+ vis_lane_objects_image.at<cv::Vec3b>(orig_h, orig_w)[1] =
+ static_cast<unsigned char>(lane_mask_color[1]);
+ vis_lane_objects_image.at<cv::Vec3b>(orig_h, orig_w)[2] =
+ static_cast<unsigned char>(lane_mask_color[2]);
}
}
}
将结果可视化后得到:
黄色为lane map结果;红色代表L_0车道线(最靠近当前车左车道线),蓝色代表R_0车道线(最靠近当前车右车道线)。判断L_0和R_0的逻辑小改了下,因为按默认的逻辑貌似会找到外边的车道线上去。
车道线后处理代码主要位于perception/obstacle/camera/lane_post_process目录。测试程序代码为cc_lane_post_processor_test.cc,先看下它的大体流程。
# 读入配置文件。
GetProtoFromFile(FLAGS_cc_lane_post_processor_config_file, &config_);
...
# 初始化CCLanePostProcessor类。
cc_lane_post_processor_->Init();
...
# 读入原图test_image.jpg。
cv::Mat test_image_ori = cv::imread(input_test_image_file, CV_LOAD_IMAGE_COLOR);
# 读入车道线语义分割结果lane_map.jpg。
cv::Mat lane_map_ori = cv::imread(input_lane_map_file, CV_LOAD_IMAGE_GRAYSCALE)
...
# 将上面读入的lane map元素转为FLOAT32。
cv::Mat lane_map;
lane_map_ori.convertTo(lane_map, CV_32FC1, 1.0f / 255.0f, 0.0f);
...
# 主处理过程,结果放于lane_objects中。
cc_lane_post_processor->Process(lane_map, lane_post_process_options, &lane_objects);
# 按预定阀值(lane_map_conf_thresh)对lane map进行过滤,并可视化。
cv::Mat vis_lane_objects_image;
for (int h = 0; h < lane_map.rows; ++h)
for (int w = 0; w < lane_map.cols, ++w) {
if (lane_map.at<float>(h, w) >= lane_map_conf_thresh)
...
}
# 可视化后处理得到的车道线对象。
for (size_t k = 0; k < lane_objects->size(); ++k) {
...
# 画车道线标识。
for (auto p = lane_objects->at(k).image_pos.begin(), p != lane_objects->at(k).image_pos.end(); ++p) {
cv::circle(vis_lane_objects_image, ...)
}
}
# 得到拟合出的多项式曲线上的点,并可视化。
...
for(float l = start; l <=end; l++) {
cv::circle(vis_lane_objects_image, ...)
}
}
# 将可视化结果存到图片中。
cv::imwrite(lane_objects_image_file, vis_lane_objects_image);
该测试程序主要包含以下几步:
可见主要的算法处理在CCLanePostProcessor类中的Init()和Process()两个函数中。下面分别看下这两个函数。它们的实现在文件cc_lane_post_processor.cc中。Init()函数中主要的步骤如下:
再来看下Process()函数,这里是算法的主要部分,大体流程图如下:
主要可分为以下几步:
其中第二步中的GenerateLaneInstance()函数步骤稍微复杂一些,再细化看下其中主要步骤:
其中的GreedyGroupConnectAssociation()函数主要步骤:
可以看到,Apollo中车道线后处理的流程还是比较长的。大体来说,首先会通过DNN得到车道线的语义分割结果,根据预定阀值转为二值图:
然后通过连通图检测和边缘检测等处理,得到车道线的内侧边缘。
接着得到Marker信息,再将这些Marker聚类分组,形成Group。这些信息会通过Graph组织起来。
之后,基于这些信息生成每条车道线对应的LaneInstance结构。最后,LaneInstance经过筛选存到LaneObject中。
这个过程相对比较长的一个原因是前面语义分割网络是对车道线所占像素进行分割,因此像虚线的各个部分在lane map中是分离的,需要在后处理中进行一系列操作将它们组合连接起来。在Apollo 3.0中加入了另一种方式,称为"Whole lane line"。看结构像有可能是位于modules/perception/model/yolo_camera_detector/lane13d_0716/目录下这个模型。看关键字可能是类似AAAI 2008上的论文《Spatial As Deep: Spatial CNN for Traffic Scene Understanding》(TuSimple车道线检测竞赛的第一名)。这种方案是在训练标注中就以整条车道线为单位,这样训练得到的网络也能输出相对完整的线,因此后处理就可以简单化很多。在Apollo的官方文档中提到,前一种方式用作视觉定位,后一种用于车道保持。