在汽车自动驾驶过程中,你需要汽车能够对周围的世界进行很好的感知。我们人类通过使用眼睛来了解汽车跑的有多快,汽车的车道线在哪,明白在哪里可以进行转弯,汽车通过摄像头,毫米波雷达,mio等其它的传感器来帮助我们实现人眼的功能。
为了让车道线检测的结果能够上车,我并没有上神经网络,而是采用特征检测方法进行测量。在阅读相关论文的方法后,我按照以下的步骤进行实验:
- 视频图片预处理。设置图片的ROI区域,对图片进行灰度转换。
- 图片边缘处理。切割ROI为若干个子图,二值化后,计算子图的连通域。
- 霍夫变换,计算图片灭点。
- 直线特征筛选,ransac 直线拟合。
- 绘制车道线。
视频图片预处理
我们知道,车道线的特征很固定,它一定在地上,而不会跑到天上去。因此,在检测的第一步,我就把图片进行了裁剪,减少了天空,树等干扰,也减少了处理图片的时间。经裁剪后的ROI ,就要转换为灰度图。其实,看了许多论文后,发现在这个步骤前,还有的对图片进行色彩空间的转换,增强黄色和白色的特征(车道线为白色或者黄色)。我在灰度转换的时候,也没有直接使用opencv的转灰度图的api, 而是提取B,G,R 三个通道值,计算I = R+G-B。读者可以实践两种转灰度的方法,会发现后者得到的灰度图更加清晰。
cv::Mat img2gray(cv::Mat img){
int height= img.rows;
int width = img.cols;
cv::Mat gray (height, width, CV_8U,cv::Scalar(0,0,0));
for (int i=0;i< height;i++) {
for (int j = 0; j < width; j++) {
int B = img.at<cv::Vec3b>(i, j)[0];
int G = img.at<cv::Vec3b>(i, j)[1];
int R = img.at<cv::Vec3b>(i, j)[2];
int I = R + G -B;
if (I > 255)
I = 255;
gray.at<uint8_t >(i, j) = I;
}
}
return gray;
}
图片的边缘处理
一提到车道线的边缘处理,一定会条件反射想到canny 边缘检测。如果只是单纯进行canny检测,那么检测结果不会太好。canny很依赖原始图片的质量,如果原来的图片车道线很分明,天气状况良好,那么canny的效果会好,但是如果图片质量不太好,那么干扰的点就会很多,这会直接影响霍夫变换的结果。因此我在canny之前对图像做了一个分割处理,对每个子图二值化,canny检测后,根据车道线的特征做连通域计算,最后将子图还原原图。
可以看出,经过这样的处理后,车道线就很清晰的呈现出来了。
霍夫变换,灭点计算
在得到上述的二值化图后,紧接着就是上opencv概率霍夫变换,检测直线。调用opencv很简单,但是注意一些参数的设定就好。
直线特征筛选,ransac 直线拟合。
并不是所有霍夫变换得到的直线都是我们的候选线 ,我们有一些筛选条件。最直接的条件是,不经过灭点的直线,一定不是车道线。所以在,霍夫变换之后,我们需要计算图片的灭点。车道线有方向角的信息,也可以通过方向角进一步筛选。由于我在图像边缘计算的时候,已经在连通域计算时候考虑车道方向角,所以这里就不再计算了。
对于候选的直线,我采用ransac算法进行拟合。如果对ransac不是很了解的,可以先看看算法的描述,我这里直接上代码了。
void ransac (Mat X ,Mat Y,std::vector<Vec2d > &result ){
int Size = X.rows;
int iters = 400;
double sigma = 0.25;
double best_k = 0.0;
double best_b = 0.0;
double k,b;
int pretotal = 2;
double P = 0.90;
int pos1,pos2;
int total_inlier;
default_random_engine e;
uniform_int_distribution<unsigned> u(0, Size);
int i;
for (i=0;i<iters;i++){
pos1= u(e);
pos2 =u(e);
int * x_1 = X.ptr<int>(pos1);
int * x_2 = X.ptr<int>(pos2);
int * y_1 = Y.ptr<int>(pos1);
int * y_2 = Y.ptr<int>(pos2);
k = (*(y_2) - *(y_1)) / (*(x_2) - *(x_1) + 0.0000001);
b = *(y_1) - k * *(x_1);
total_inlier= 0;
for (int j=0;j<Size;j++){
double y_estimate = k * *(X.ptr<int>(j)) +b;
if (fabs(y_estimate-*(Y.ptr<int>(j)))<sigma){
total_inlier+= 1;
}
}
if (total_inlier>pretotal){
iters = log(1-P)/log( 1-pow(total_inlier/(Size+0.0000001) ,2.0 ));
pretotal = total_inlier;
best_k = k;
best_b = b;
}
if (total_inlier > (Size/2)){
break;
}
}
result.push_back(Vec2d(best_k,best_b));
}
绘制直线
最重要的检测部分已经完成了,那么绘制车道线就很轻松了,通过简单的几行代码,就可以把我们的直线绘制出来了。
void draw_lane(std::vector<cv::Vec4i>coordinate)
{
std::cout << coordinate.size()<< std::endl;
for (auto it = coordinate.begin();it!= coordinate.end();it ++){
line(img, cv::Point((*it)[0], (*it)[1]), cv::Point((*it)[2], (*it)[3]), cv::Scalar(0,255,0), 3);
}
imshow("lane ",img);
cv::waitKey(1)
}
下图就是最后的检测结果啦~。其中,红色圆点表示灭点位置??煽闯?,车道线已经比较好的拟合出来,但是左右两边还是有杂线,这就需要进一步筛选过滤了。