基于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 );
//InputArray image:输入图像,必须是8位单通道图像。
//OutputArray lines:输出检测到的线条参数集合。
//double rho:直线搜索时的距离r步长,以像素为单位,一般为1。
//double theta:直线搜索时的角度θ步长,以弧度为单位,一般为CV_PI/180。
//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 );
  
//InputArray image:输入图像,必须是8位单通道图像。
//OutputArray lines:检测到的线条参数集合。
//double rho:以像素为单位的距离r步长,一般为1。。
//double theta:以弧度为单位的角度θ步长,一般为CV_PI/180。
//int threshold:累加计数值的阈值参数,当参数空间某个交点的累加计数的值超过该阈值,则认为该交点对应了图像空间的一条直线。
//double srn:默认值为0,用于在多尺度霍夫变换中作为参数rho的除数,rho=rho/srn。
//double stn:默认值为0,用于在多尺度霍夫变换中作为参数theta的除数,theta=theta/stn。
//如果srn和stn同时为0,就表示HoughLines函数执行标准霍夫变换,否则就是执行多尺度霍夫变换。

lines 参数为r,θ,一般定义为 vector<Vec2f> lines;

霍夫线变换部分原理

极坐标方式作为参数空间。图像坐标系到极坐标系参数空间的转换过程

md35

从上面可以看到,参数空间的每个点(ρ,θ)都对应了图像空间的一条直线,换个角度说就是**图像空间的一个点在参数空间中就对应为一条曲线。**参数空间采用极坐标系,这样就可以在参数空间表示原始空间中的所有直线了。

极坐标系参数空间到图像坐标系的转换过程:

md36

md37

这样就把在图像空间中检测直线的问题转化为在极坐标参数空间中找通过点(r,θ)的最多正弦曲线数的问题。霍夫空间中,曲线的交点次数越多,所代表的参数越确定,画出的图形越饱满。

实际上,图像空间的一个点并不能确认一条直线,可以对应无数条直线,因此在实际应用中,我们必须用各种条件限定直线的数量。

我们将直线的方向θ离散化为有限个等间距的离散值,参数ρ也就对应离散化为有限个值,于是参数空间不再是连续的,而是被离散量化为一个个等大小网格单元。将图像空间(直角坐标系)中每个像素点坐标值变换到参数空间(极坐标系)后,所得值会落在某个网格内,使该网格单元的累加计数器加1。当图像空间中所有的像素都经过霍夫变换后,对网格单元进行检查,累加计数值最大的网格,其坐标值(ρ0ρ_0, θ0θ_0)就对应图像空间中所求的直线。

离散化过程:

md38

霍夫直线检测的优缺点

  • 优点:Hough直线检测的优点是抗干扰能力强,对图像中直线的残缺部分、噪声以及其它共存的非直线结构不敏感,能容忍特征边界描述中的间隙,并且相对不受图像噪声的影响

  • 缺点:Hough变换算法的特点导致其时间复杂度和空间复杂度都很高,并且在检测过程中只能确定直线方向,丢失了线段的长度信息。由于霍夫检测过程中进行了离散化,因此检测精度受参数离散间隔制约

直线拟合函数 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 );
//InputArray points, 输入需要拟合的直线点集,一般为vector<Point> left_point。
//OutputArray line, 输出的直线向量,一般为Vec4f left_line
//int distType, 拟合直线采用的方法
//double param, 距离参数,一般为。
//double reps, 径向的精度,通常为1e-2即0.01。
//double aeps 角度的精度,通常为1e-2即0.01。



line 的形式为 cos sin x y 夹角和一个坐标点

distType的类型:md39

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();
/** full constructor
@param center The rectangle mass center.
@param size Width and height of the rectangle.
@param angle The rotation angle in a clockwise direction. When the angle is 0, 90, 180, 270 etc.,
the rectangle becomes an up-right rectangle.
*/
RotatedRect(const Point2f& center, const Size2f& size, float angle);
/**
Any 3 end points of the RotatedRect. They must be given in order (either clockwise or anticlockwise).
*/
RotatedRect(const Point2f& point1, const Point2f& point2, const Point2f& point3);

/** returns 4 vertices of the rectangle
@param pts The points array for storing rectangle vertices. The order is bottomLeft, topLeft, topRight, bottomRight.
*/
void points(Point2f pts[]) const;
//! returns the minimal up-right integer rectangle containing the rotated rectangle
Rect boundingRect() const;
//! returns the minimal (exact) floating point rectangle containing the rotated rectangle, not intended for use with images
Rect_<float> boundingRect2f() const;
//! returns the rectangle mass center
Point2f center;
//! returns width and height of the rectangle
Size2f size;
//! returns the rotation angle. When the angle is 0, 90, 180, 270 etc., the rectangle becomes an up-right rectangle.
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

实验步骤

方法一

  1. 预处理图像
    1. 读取视频帧。对视频的每一帧图像进行处理。
    2. 去噪声。高斯滤波器。
    3. 灰度化处理。颜色信息对车道线检测关系不大。
    4. 二值化处理。便于后续提取车道线
  2. 提取ROI区域
    1. 提取车道线。通过画图工具找坐标,调整参数将车道ROI区域截取处理。、
    2. 轮廓处理。对ROI区域进行轮廓处理
  3. 绘制车道线
    1. 优化轮廓。对提取出来的轮廓内的坐标进行处理,一般只提取最大值和最小值。
    2. 绘制直线。根据优化后的坐标进行绘制车道线。

这种方法在绘制车道线时,主要是通过寻找轮廓函数findContours里面的返回值,对返回值进行提取角度、坐标最小值和最大值,从而进行优化并绘制直线。

方法二

  1. 预处理图像
    1. 读取视频帧。对视频的每一帧图像进行处理。
    2. 灰度化处理。颜色信息对车道线检测关系不大。
    3. 边缘检测。Canny检测,由于内部已经有进行高斯滤波处理,因此不用再单独进行滤波处理了。
  2. 提取ROI区域
    1. 提取车道线。通过画图工具找坐标,调整参数将车道ROI区域截取处理。
  3. 对车道线处理
    1. 霍夫直接检测。取出所有直线。
    2. 优化处理直线。根据直线斜率判断、根据分布分为左右边车道线、最小二乘法拟合直线。
  4. 绘制车道线
    1. 根据优化后的直线参数,计算坐标绘制直线。

代码

方法一

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("G:\\opencv-python-image\\images\\lane.avi");
//VideoCapture capture("C:\\Users\\Obito\\Desktop\\project_video.mp4");
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); // 自适应阈值二值化
//Canny(gray, binary, 50, 150);
//imshow("binary", binary);

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]); // 最小外接矩形 车道线相对其他物体向小
// RotatedRect类对象有三个重要属性: 矩形质心 边长(长宽) 旋转角度
int angle = abs(rrt.angle);
if (angle < 20 || angle > 160 || angle == 90) // 旋转角度再0 90 180 附近,矩形就成了一个直立的矩形
continue;
printf("rrt.angle: %.2f\n", rrt.angle);
/*Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255));
drawContours(frame, contours, (int)i, color, 2, 8, hierarchy, 0, Point());*/
/*cout << "contours.size" << contours.size() << endl;
cout << "contours[i].size" << contours[i].size() << endl;*/
Point pt1(-1, -1);
Point pt2(-1, -1);
int miny = 10000;
int maxy = 0;
for (int p = 0; p < contours[i].size(); p++) // 找出每个轮廓内的最小和最大的y值所对应的坐标,后面可以画线
{
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("G:\\opencv-python-image\\images\\lane.avi");
//VideoCapture capture("C:\\Users\\Obito\\Desktop\\project_video.mp4");
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; // tmp_line 存放筛选斜率后的直线 left、right则是进一步分类存储 Vec4i类型 x1 x2 y1 y2
vector<Point> left_point, right_point; // 存放两边直线的坐标 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))
{
// 斜率大于0且 x坐标在图像中线右边 归为右边车道线 注意:opencv的图像y轴是朝下
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))
{
// 斜率小于0且 x坐标在图像中线左边 归为左边车道线 注意:opencv的图像y轴是朝下
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;
// 规定两个线的y值 初始y为最底,然后延长150
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;
// 由 y = k(x - x0) + y0 可得 x = (y - y0) / k + x
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;
// 由 y = k(x - x0) + y0 可得 x = (y - y0) / k + x
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);
}

}

实验图

实验是对视频进行检测,这里为了方便只截取图像,另外给出原视频链接。

image-20230809231624602

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边缘检测,很好的去除了一些无关信息,能够有效的提取车道线轮廓。

其次方法二采用了霍夫直线检测加上最小二乘法拟合,可以更好地绘制出车道线;而方法一只是对轮廓内的坐标点进行最值判断,直接根据最大值和最小值画出直线,可能会不准,并且是对轮廓进行遍历,因此会同时出现多条直线的情况。

要点

  1. 根据车道位置对ROI区域的提取,便于后续的操作。
  2. 边缘检测时的阈值大小。
  3. 霍夫直线检测时斜率的判断。
  4. 根据不同函数对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博客