基于Opencv的车道线检测
前言:
这只是一个简单的检测,并不具有普遍性。车道线检测的应用很广泛,小到一些智能小车的巡线功能,大到无人驾驶巡线功能等,一般用于小车运动应用,但是在不同场景下,它们的需求大不相同,因此也就需要不同的技术去实现。本次实验采用opencv来实现,主要是用来复习巩固自己所学知识,同时由于是基于opencv实现的传统方法,没有运用一些模型来训练等,因此项目并不具有普遍性,需要根据不同视频进行调参。
实验环境
Window 10、Visual Studio 2022、 opencv4.6
涉及到的一些知识
霍夫变换
霍夫变换(Hough Transform)是图像处理中的一种特征提取技术,该过程在一个参数空间中通过计算累计结果的局部最大值得到一个符合该特定形状的集合作为霍夫变换结果。
Opencv中的霍夫线变换
霍夫线变换是一种用来寻找直线的方法。在使用霍夫线变换之前,首先要对图像进行边缘检测的处理,因此霍夫线变换的输入必须是二值图像 。
OpenCV支持三种不同的霍夫线变换
标准霍夫变换(Standard Hough Transform,SHT)
多尺度霍夫变换(Multi-Scale Hough Transform,MSHT)
累计概率霍夫变换(Progressive Probabilistic Hough Transform ,PPHT)
其中,多尺度霍夫变换(MSHT)为经典霍夫变换(SHT)在多尺度下的一个变种。累计概率霍夫变换(PPHT)算法是标准霍夫变换(SHT)算法的一个改进,它在一定的范围内进行霍夫变换,计算单独线段的方向以及范围,从而减少计算量,缩短计算时间。之所以称PPHT为“概率”的,是因为并不将累加器平面内的所有可能的点累加,而只是累加其中的一部分 ,该想法是如果峰值如果足够高,只用一小部分时间去寻找它就够了,可以实质性地减少计算时间。
在OpenCV中,我们可以用HoughLines函数来调用标准霍夫变换SHT和多尺度霍夫变换MSHT。
而HoughLinesP函数用于调用累计概率霍夫变换PPHT。累计概率霍夫变换执行效率很高,所有相比于HoughLines函数,我们更倾向于使用HoughLinesP函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 CV_EXPORTS_W void HoughLinesP ( InputArray image, OutputArray lines, double rho, double theta, int threshold, double minLineLength = 0 , double maxLineGap = 0 ) ; r和θ步长越小,搜索时间越长。 lines 参数为直线起点和终点 x1,y1,x2,y2 因此一般定义为vector<Vec4i> lines;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 CV_EXPORTS_W void HoughLines ( InputArray image, OutputArray lines, double rho, double theta, int threshold, double srn = 0 , double stn = 0 , double min_theta = 0 , double max_theta = CV_PI ) ; lines 参数为r,θ,一般定义为 vector<Vec2f> lines;
霍夫线变换部分原理
极坐标方式作为参数空间。图像坐标系到极坐标系参数空间的转换过程 :
从上面可以看到,参数空间的每个点(ρ,θ)都对应了图像空间的一条直线 ,换个角度说就是**图像空间的一个点在参数空间中就对应为一条曲线。**参数空间采用极坐标系,这样就可以在参数空间表示原始空间中的所有直线了。
极坐标系参数空间到图像坐标系的转换过程:
这样就把在图像空间中检测直线的问题转化为在极坐标参数空间中找通过点(r,θ)的最多正弦曲线数的问题。霍夫空间中,曲线的交点次数越多,所代表的参数越确定,画出的图形越饱满。
实际上,图像空间的一个点并不能确认一条直线,可以对应无数条直线,因此在实际应用中,我们必须用各种条件限定直线的数量。
我们将直线的方向θ离散化为有限个等间距的离散值,参数ρ也就对应离散化为有限个值,于是参数空间不再是连续的,而是被离散量化为一个个等大小网格单元。将图像空间(直角坐标系)中每个像素点坐标值变换到参数空间(极坐标系)后,所得值会落在某个网格内,使该网格单元的累加计数器加1。当图像空间中所有的像素都经过霍夫变换后,对网格单元进行检查,累加计数值最大的网格,其坐标值(ρ 0 ρ_0 ρ 0 , θ 0 θ_0 θ 0 )就对应图像空间中所求的直线。
离散化过程:
霍夫直线检测的优缺点
直线拟合函数 fitLine()
1 2 3 4 5 6 7 8 9 10 11 12 CV_EXPORTS_W void fitLine ( InputArray points, OutputArray line, int distType, double param, double reps, double aeps ) ; line 的形式为 cos sin x y 夹角和一个坐标点
distType的类型:
RotatedRect类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class CV_EXPORTS RotatedRect{ public : RotatedRect (); RotatedRect (const Point2f& center, const Size2f& size, float angle); RotatedRect (const Point2f& point1, const Point2f& point2, const Point2f& point3); void points (Point2f pts[]) const ; Rect boundingRect () const ; Rect_<float > boundingRect2f () const ; Point2f center; Size2f size; float angle; }; 该类有中心点center、尺寸size (width, height)、旋转角度angle三个成员
该类中的旋转角度暂时没搞懂怎么看的,查了许多资料都挺乱的,待定。
OpenCV之RotatedRect基本用法和角度探究_opencv rotatedrect_sandalphon4869的博客-CSDN博客
https://stackoverflow.com/questions/15956124/minarearect-angles-unsure-about-the-angle-returned/21427814#21427814
https://stackoverflow.com/questions/24073127/opencvs-rotatedrect-angle-does-not-provide-enough-information
实验步骤
方法一
预处理图像
读取视频帧。对视频的每一帧图像进行处理。
去噪声。高斯滤波器。
灰度化处理。颜色信息对车道线检测关系不大。
二值化处理。便于后续提取车道线
提取ROI区域
提取车道线。通过画图工具找坐标,调整参数将车道ROI区域截取处理。、
轮廓处理。对ROI区域进行轮廓处理
绘制车道线
优化轮廓。对提取出来的轮廓内的坐标进行处理,一般只提取最大值和最小值。
绘制直线。根据优化后的坐标进行绘制车道线。
这种方法在绘制车道线时,主要是通过寻找轮廓函数findContours里面的返回值,对返回值进行提取角度、坐标最小值和最大值,从而进行优化并绘制直线。
方法二
预处理图像
读取视频帧。对视频的每一帧图像进行处理。
灰度化处理。颜色信息对车道线检测关系不大。
边缘检测。Canny检测,由于内部已经有进行高斯滤波处理,因此不用再单独进行滤波处理了。
提取ROI区域
提取车道线。通过画图工具找坐标,调整参数将车道ROI区域截取处理。
对车道线处理
霍夫直接检测。取出所有直线。
优化处理直线。根据直线斜率判断、根据分布分为左右边车道线、最小二乘法拟合直线。
绘制车道线
根据优化后的直线参数,计算坐标绘制直线。
代码
方法一
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 #include <opencv2/opencv.hpp> #include <iostream> using namespace cv;using namespace std;RNG rng (12345 ) ;void DetectRoadLine (Mat& frame) ;int main () { VideoCapture capture ("C:\\Users\\Obito\\Desktop\\1.mp4" ) ; if (!capture.isOpened ()) { cout << "could not load video file!" << endl; system ("pause" ); return -1 ; } Mat frame; while (capture.read (frame)) { char key = waitKey (100 ); if (key == 27 ) break ; imshow ("BeforeProcessed" , frame); DetectRoadLine (frame); imshow ("AfterProcessed" , frame); } waitKey (0 ); return 0 ; } void DetectRoadLine (Mat& frame) { int offx = frame.cols / 5 ; int offy = frame.rows / 3 ; Rect rect; rect.x = offx + 30 ; rect.y = frame.rows - offy - 20 ; rect.width = frame.cols - offx * 1.5 ; rect.height = offy - 50 ; Mat copy = frame (rect).clone (); imshow ("copy" , copy); Mat gray, binary; GaussianBlur (copy, copy, Size (3 , 3 ), 0 ); cvtColor (copy, gray, COLOR_BGR2GRAY); threshold (gray, binary, 0 , 255 , THRESH_BINARY | THRESH_OTSU); Mat mask = Mat::zeros (frame.size (), CV_8UC1); binary.copyTo (mask (rect)); vector<vector<Point>> contours; vector<Vec4i>hierarchy; findContours (mask, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE, Point (0 , 0 )); Mat drawing = Mat::zeros (mask.size (), CV_8UC3); for (size_t i = 0 ; i < contours.size (); i++) { RotatedRect rrt = minAreaRect (contours[i]); int angle = abs (rrt.angle); if (angle < 20 || angle > 160 || angle == 90 ) continue ; printf ("rrt.angle: %.2f\n" , rrt.angle); Point pt1 (-1 , -1 ) ; Point pt2 (-1 , -1 ) ; int miny = 10000 ; int maxy = 0 ; for (int p = 0 ; p < contours[i].size (); p++) { Point tmp = contours[i][p]; if (miny > tmp.y) { miny = tmp.y; pt1 = tmp; } if (maxy < tmp.y) { maxy = tmp.y; pt2 = tmp; } } if (pt1.x < 0 || pt2.x < 0 ) continue ; printf ("line Point1 (x = %d, y = %d) to Point2 (x=%d, y=%d)\n" , pt1.x, pt1.y, pt2.x, pt2.y); line (frame, pt1, pt2, Scalar (255 , 0 , 0 ), 3 , 8 ); } imshow ("mask" , mask); }
方法二
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 #include <opencv2/opencv.hpp> #include <iostream> using namespace cv;using namespace std;double slope_th = 0.3 ; double canny_threshold1 = 120 ;double canny_threshold2 = 200 ;RNG rng (12345 ) ;void DetectRoadLine (Mat& frame) ;int main () { VideoCapture capture ("C:\\Users\\Obito\\Desktop\\1.mp4" ) ; if (!capture.isOpened ()) { cout << "could not load video file!" << endl; system ("pause" ); return -1 ; } Mat frame; while (capture.read (frame)) { char key = waitKey (100 ); if (key == 27 ) break ; imshow ("BeforeProcessed" , frame); DetectRoadLine (frame); imshow ("AfterProcessed" , frame); } waitKey (0 ); return 0 ; } void DetectRoadLine (Mat& frame) { int offx = frame.cols / 5 ; int offy = frame.rows / 3 ; Rect rect; rect.x = offx + 50 ; rect.y = frame.rows - offy ; rect.width = frame.cols - offx * 2 ; rect.height = offy - 50 ; Mat copy = frame (rect).clone (); imshow ("copy" , copy); Mat gray, canny; cvtColor (copy, gray, COLOR_BGR2GRAY); Canny (gray, canny, canny_threshold1, canny_threshold2); imshow ("canny" , canny); Mat mask = Mat::zeros (frame.size (), CV_8UC1); canny.copyTo (mask (rect)); vector<Vec4i> lines; HoughLinesP (mask, lines, 2 , CV_PI / 180 , 50 , 20 , 50 ); vector<double > slope_all; vector<Vec4i> tmp_line, left, right; vector<Point> left_point, right_point; Point pt1, pt2; for (auto i : lines) { pt1 = Point (i[0 ], i[1 ]); pt2 = Point (i[2 ], i[3 ]); double slope = (static_cast <double >(pt1.y) - static_cast <double >(pt2.y)) / (static_cast <double >(pt1.x) - static_cast <double >(pt2.x)); if (abs (slope) > slope_th) { slope_all.push_back (slope); tmp_line.push_back (i); cout << "slope:" << slope << endl; } } for (size_t i = 0 ; i < tmp_line.size (); i++) { pt1 = Point (tmp_line[i][0 ], tmp_line[i][1 ]); pt2 = Point (tmp_line[i][2 ], tmp_line[i][3 ]); if (slope_all[i] > 0 && pt1.x > (frame.cols / 2 ) && pt2.x > (frame.cols / 2 )) { right.push_back (tmp_line[i]); right_point.push_back (pt1); right_point.push_back (pt2); } else if (slope_all[i] < 0 && pt1.x < (frame.cols / 2 ) && pt2.x < (frame.cols / 2 )) { left.push_back (tmp_line[i]); left_point.push_back (pt1); left_point.push_back (pt2); } cout << "left_point.size:" << left_point.size () << endl; cout << "right_point.size:" << right_point.size () << endl; } Vec4f left_line, right_line; int y1 = frame.rows; int y2 = y1 - 150 ; int x1_right, x2_right, x1_left, x2_left; if (left_point.size () != 0 ) { fitLine (left_point, left_line, DIST_L2, 0 , 0.01 , 0.01 ); double left_k = left_line[1 ] / left_line[0 ]; cout << "左侧车道线斜率: " << left_k << endl; x1_left = ((y1 - static_cast <double >(left_line[3 ])) / left_k + static_cast <double >(left_line[2 ])); x2_left = ((y2 - static_cast <double >(left_line[3 ])) / left_k + static_cast <double >(left_line[2 ])); cout << "x1_left:" << x1_left << endl; cout << "x2_left:" << x2_left << endl; cout << "left_line[2]:" << left_line[2 ] << endl; cout << "left_line[3]:" << left_line[3 ] << endl; line (frame, Point (x1_left, y1), Point (x2_left, y2), Scalar (255 , 0 , 0 ), 8 ); } if (right_point.size () != 0 ) { fitLine (right_point, right_line, DIST_L2, 0 , 0.01 , 0.01 ); double right_k = right_line[1 ] / right_line[0 ]; cout << "右侧车道线斜率: " << right_k << endl; x1_right = ((y1 - static_cast <double >(right_line[3 ])) / right_k + static_cast <double >(right_line[2 ])); x2_right = ((y2 - static_cast <double >(right_line[3 ])) / right_k + static_cast <double >(right_line[2 ])); cout << "x1_right:" << x1_right << endl; cout << "x2_right:" << x2_right << endl; cout << "right_line[2]:" << right_line[2 ] << endl; cout << "right_line[3]:" << right_line[3 ] << endl; line (frame, Point (x1_right, y1), Point (x2_right, y2), Scalar (0 , 0 , 255 ), 8 ); } }
实验图
实验是对视频进行检测,这里为了方便只截取图像,另外给出原视频链接。
https://github.com/kkk324/Vehicle-Distance-Detection
https://www.bilibili.com/video/BV19J411t7Ss/?spm_id_from=333.880.my_history.page.click&vd_source=170970c6528ce8b04fb449814b901de5
比较
两个方法相比较下,第二个方法更优;
首先方法一只是单纯截取了ROI区域然后进行简单的二值化处理,当ROI区域内有很多其他噪声,效果会很差;方法二采用了Canny边缘检测,很好的去除了一些无关信息,能够有效的提取车道线轮廓。
其次方法二采用了霍夫直线检测加上最小二乘法拟合,可以更好地绘制出车道线;而方法一只是对轮廓内的坐标点进行最值判断,直接根据最大值和最小值画出直线,可能会不准,并且是对轮廓进行遍历,因此会同时出现多条直线的情况。
要点
根据车道位置对ROI区域的提取,便于后续的操作。
边缘检测时的阈值大小。
霍夫直线检测时斜率的判断。
根据不同函数对vector容器类型的选择。
参考资料
OpenCV C++案例实战一《车道检测》_车道线检测c++_Zero___Chen的博客-CSDN博客
基于OpenCV的车道线检测算法(Traditional Method) - 知乎 (zhihu.com)
https://blog.csdn.net/Zero___Chen/article/details/119666454
c++ 车道线检测_c++车道线代码实现_majinbuu鲸落的博客-CSDN博客
霍夫变换直线检测(Line Detection)原理及示例_霍夫变换直线检测原理_leonardohaig的博客-CSDN博客
[【OpenCV入门教程之十四】OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑_1][原]【opencv入门教程之十四】opencv霍夫变换:霍夫线变换,霍夫圆变换合辑_浅墨_毛星云的博客-CSDN博客