作者 | 李秋键
责编 | 寇雪芹
出品 | AI科技大本营(ID:rgznai100)
引言:
本文将利用opencv实现对复杂场景下车道线的实时检测;所使用的图像处理方法主要是在读取图片的基础上,进行多种边缘检测,然后对不同的检测结果进行融合以提取出道路图像,去除其他噪声。然后对提取的连通区域进行判断,找寻最大连通区域最终定为提取的道路。然后根据提取的道路图像,再次利用边缘检测,提取车道线信息,然后利用透视变换将视角变成俯视图,其中透视变换矩阵的四个点由提取道路图像的角点组成。然后对俯视图进行滑动窗口多项式拟合画出车道线,并显示图片和保存成视频!文末附源码。
图1 效果图
系统概述
1.1 对所给数据图像的车道线进行检测。
其中所给数据图像如下图可见:
图2 数据图像
下面我将对所用到的功能和原理将分别阐述。
(1)边缘特征提取:
边缘特征的提取使用的是多种边缘检测算法,其中包括Sobel单方向梯度算子函数、hls阈值分割、lab阈值分割、luv阈值分割、Sobel多方向阈值分割、Sobel多方向梯度算子函数。然后对他们提取的特征进行融合作为最终提取的特征信息。如下图可见,分别是各自边缘特征算法提取出的特征图像
(2)特征融合:
根据不同算法提取到的特征,对其中我们需要保留的特征筛选,即保留白色共同区域。
图3 融合效果图
(3)最大连通区域:
根据所提取到的特征融合图像,可以知道我们需要寻找的是最大的白色区域,此时正好是为道路特征。为了防止过远处的干扰,对图像的一半进行判断即可。
图4 最大连通区域图
然后对提取到的最大连通区域进行原始像素值覆盖即可保留原来的道路图像。
图5 提取的道路图
(4)道路提取图像再次边缘检测:
利用拉普拉斯算子再次对处理后的图像进行边缘检测。并对其进行腐蚀和膨胀消除噪声。
图6道路拉普拉斯边缘提取图
图7道路边缘去除噪声图
(5)对道路区域寻找角点:
建立新的变量使得道路区域变成白色,其他区域为黑色,来提取角点坐标。
图8 道路提取角点图
(6)对道路部分图像透视变换:
在找到角点后,就可以利用角点坐标计算变换矩阵。并对图像进行透视变换操作。
图9 透视变换转为俯视图
(7)滑动窗口多项式拟合曲线
通过对透视变换后的图像计算统计直方图,确认三个车道的坐标,然后利用滑动窗口进行拟合。
图10 滑动窗口拟合图
(8)直线拟合窗口绘制:
在获取到三车道的坐标后,分别将不同车道赋值不同颜色。但是考虑到又的时候只能找到两个车道或没有车道,需要加入一系列判断。
图11 直线拟合滑动窗口图
(9)转回原始图片处理:
再次透视变换反变换回原图,并对图片进行判断覆盖掉黑色和白色像素点。但是其中边缘检测造成某些值不都是黑白,所以加上范围判断赋值。
图12 原图绘制恢复效果
1.2 GUI窗口的搭建:
对图像处理后保存的视频读取显示,与按钮控件绑定即可。主要功能有训练保存视频、显示视频和关闭窗口功能。
图13 程序布局图
(1)识别训练功能:
当点击识别训练时,会弹出cmd命令行,并显示处理过程。
图14 视频训练点击效果图
(2)视频播放功能:
当结束cmd执行过程后,会在本地文件夹下生成四个视频文件,这时候点击播放按钮就可以在GUI上播放视频。
图15 视频播放按钮点击效果图
并在点击按钮后,更换按钮颜色,此时按钮变成“关闭”,可以关闭视频播放
图16 关闭按钮点击效果图
(3)退出界面功能:
点击退出后会弹窗是否退出,点击退出即可。
图17 退出按钮点击效果图
代码功能实现
2.1 系统环境描述:
系统所使用的环境是python3.6.5,opencv3.14.8版本,windows10系统。编程工具使用的是pycharm专业版。所用到的python其他库有os,在这里用来寻找本地图片文件等操作;numpy库用来当对读取到的图片矩阵进行运算处理;pyqt5库用来创建GUI窗口程序等。
2.2 功能模块划分:
按照前面所描述的,所划分的模块总的可以分为图像处理模式识别模块和GUI窗口程序两个部分。其中每个部分又可以分为好几个部分,具体已经在前面有所阐述。
2.3 实现原理:
利用图像处理技术,分割出道路图像,然后对分割出的道路图像再次边缘检测,找出车道,然后透视变换和滑动窗口拟合成曲线,然后处理显示在原场景下。
功能模块的程序实现
3.1 图像处理模式识别部分:
(1)sobel算子函数单方向梯度边缘检测:
Sobel算子是一种一阶微分算子,它利用像素邻近区域的梯度值来计算1个像素的梯度,然后根据一定的绝对值来取舍。Sobel算子使用的是3*3算子模板。这里分别用的是x方向的和多方向的不同计算阈值算子。最终运算结果是一幅边缘幅度图像。对应代码如下:
代码语言:javascript复制1#Sobel算子函数
2def abs_sobel_thresh(img, orient='x', thresh_min=10, thresh_max=100):
3 # 转换为灰度图
4 gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
5 # 使用OpenCV Sobel()函数应用x或y梯度然后取绝对值
6 if orient == 'x':
7 abs_sobel = np.absolute(cv2.Sobel(gray, cv2.CV_64F, 1, 0))
8 if orient == 'y':
9 abs_sobel = np.absolute(cv2.Sobel(gray, cv2.CV_64F, 0, 1))
10 # 重新缩放回8位整数
11 scaled_sobel = np.uint8(255 * abs_sobel / np.max(abs_sobel))
12 # 创建一个副本并应用阈值
13 binary_output = np.zeros_like(scaled_sobel)
14 # 这里我使用的是包容性(>=,<=)阈值
15 # print(scaled_sobel[:,50])
16 binary_output[(scaled_sobel >= thresh_min) & (scaled_sobel <= thresh_max)] = 255
17 #cv2.imshow("abs_sobel_thresh", binary_output)
18 return binary_output
19def dir_threshold(img, sobel_kernel=3, thresh=(0.8, np.pi / 2)):
20 gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
21 # 计算x和y的梯度
22 sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=sobel_kernel)
23 sobely = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=sobel_kernel)
24 # 取梯度方向的绝对值
25 # 应用一个阈值,并创建一个二值图像结果
26 absgraddir = np.arctan2(np.absolute(sobely), np.absolute(sobelx))
27 # print(absgraddir[:,50])
28 binary_output = np.zeros_like(absgraddir)
29 binary_output[(absgraddir >= thresh[0]) & (absgraddir <= thresh[1])] = 255
30 #cv2.imshow("dir_threshold", binary_output)
31 return binary_output
32def mag_thres(img, sobel_kernel=9, mag_thresh=(20, 255)):
33 # 转换为灰度图
34 gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
35 # 使用Sobel x和y梯度
36 sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=sobel_kernel)
37 sobely = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=sobel_kernel)
38 # 计算梯度大小
39 gradmag = np.sqrt(sobelx ** 2 sobely ** 2)
40 # 重新缩放到8位
41 scale_factor = np.max(gradmag) / 255
42 gradmag = (gradmag / scale_factor).astype(np.uint8)
43 # print(gradmag[:, 50])
44 # 创建一个二值图像,在阈值满足时为255,否则为0
45 binary_output = np.zeros_like(gradmag)
46 binary_output[(gradmag >= mag_thresh[0]) & (gradmag <= mag_thresh[1])] = 255
47 #cv2.imshow("mag_thresh", binary_output)
48 return binary_output
图18 Sobel函数挑选效果图
(2)各种阈值分割:
这里使用了hls、Lab和LUV空间的阈值分割。通过尝试不同的参数,最终选择的参数在代码部分可以很清晰看出为110到255之间。
HLS 和 HSV 的区别就是最后一个分量不同,HLS 的是 light(亮度),HSV 的是 value(明度)。可以到这个 网页 尝试一下。
HLS 中的 L 分量为亮度,亮度为100,表示白色,亮度为0,表示黑色;HSV 中的 V 分量为明度,明度为100,表示光谱色,明度为0,表示黑色。
下面是 HLS 颜色空间圆柱体:
图19 HSL颜色空间图
Lab是由一个亮度通道(channel)和两个颜色通道组成的。在Lab颜色空间中,每个颜色用L、a、b三个数字表示,各个分量的含义是这样的:
L*代表亮度
a*代表从绿色到红色的分量
b*代表从蓝色到黄色的分量
图20 LAB颜色空间图
LUV色彩空间全称CIE 1976(L*,u*,v*)(也作CIELUV)色彩空间,L*表示物体亮度,u*和v*是色度。于1976年由国际照明委员会(International Commission on Illumination)提出,由CIE XYZ空间经简单变换得到,具视觉统一性。类似的色彩空间有CIELAB。对于一般的图像,u*和v*的取值范围为-100到 100,亮度为0到100。
图21 LUV颜色空间图
对应代码如下:
代码语言:javascript复制 1def hls_select(img, channel='l', thresh=(110, 255)):
2 hls = cv2.cvtColor(img, cv2.COLOR_RGB2HLS)
3 if channel == 'h':
4 channel = hls[:, :, 0]
5 elif channel == 'l':
6 channel = hls[:, :, 1]
7 else:
8 channel = hls[:, :, 2]
9 binary_output = np.zeros_like(channel)
10 # print(channel[:,50])
11 binary_output[(channel > thresh[0]) & (channel <= thresh[1])] = 255
12 #cv2.imshow("hls_select", binary_output)
13 return binary_output
14def lab_select(img, thresh=(110, 255)):
15 lab = cv2.cvtColor(img, cv2.COLOR_RGB2Lab)
16 b_channel = lab[:, :, 0]
17 # print(b_channel[:,50])
18 binary_output = np.zeros_like(b_channel)
19 binary_output[(b_channel > thresh[0]) & (b_channel <= thresh[1])] = 255
20 #cv2.imshow("lab_select", binary_output)
21 return binary_output
22def luv_select(img, thresh=(110, 255)):
23 luv = cv2.cvtColor(img, cv2.COLOR_RGB2LUV)
24 l_channel = luv[:, :, 0]
25 # print(l_channel[:,50])
26 binary_output = np.zeros_like(l_channel)
27 binary_output[(l_channel > thresh[0]) & (l_channel <= thresh[1])] = 255
28 #cv2.imshow("luv_select", binary_output)
29 return binary_output
(3)阈值融合:
再进行完不同的边缘检测后,将各自的结果共同的白色区域保留。代码如下:
代码语言:javascript复制 1def thresholding(img):
2 x_thresh = abs_sobel_thresh(img, orient='x', thresh_min=10, thresh_max=100)
3 mag_thresh = mag_thres(img, sobel_kernel=9, mag_thresh=(20, 255))
4 dir_thresh = dir_threshold(img, sobel_kernel=3, thresh=(0.7, 1.3))
5 hls_thresh = hls_select(img, channel='l', thresh=(110, 255))
6 lab_thresh = lab_select(img, thresh=(110, 255))
7 luv_thresh = luv_select(img, thresh=(110, 255))
8 threshholded = np.zeros_like(x_thresh)
9 threshholded[((x_thresh == 255) & (mag_thresh == 255)) | ((dir_thresh == 255) & (hls_thresh == 255)) | (
10 lab_thresh == 255) | (luv_thresh == 255)] = 255
11 #cv2.imshow("thresholding", threshholded)
12 return threshholded
(4)找出最大连通区域为道路位置:
代码语言:javascript复制 1abel0 = abs1[int(abs1.shape[0] / 2):abs1.shape[0], :]
2label_img, number = measure.label(label0, neighbors=8, background=0, return_num=True, connectivity=2)
3'''找出最大连通区域'''
4max_label = 1
5max_num = 0
6for i in range(1, number 1):
7 if np.sum(label_img == i) > max_num:
8 max_num = np.sum(label_img == i)
9 max_label = i
10dst = color.label2rgb(label_img)
11for i in range(label_img.shape[0]):
12 for j in range(label_img.shape[1]):
13 if label_img[i, j] == max_label:
14 abs1[int(abs1.shape[0] / 2) i, j] = img[int(abs1.shape[0] / 2) i, j][2]
15 mid_img[int(abs1.shape[0] / 2) i, j] = 255
16 else:
17 abs1[int(abs1.shape[0] / 2) i, j] = 0
18 mid_img[int(abs1.shape[0] / 2) i, j] = 0
19#道路特征图
20#cv2.imshow("road", abs1)
(5)角点寻找:
创建一个新的变量,保留着道路部分为白色,其他部分为黑色,用多边形拟合寻找角点。
代码语言:javascript复制 1mid_img = cv2.erode(mid_img, kernel, iterations=1)
2cv2.imshow("cut", abs1)
3for i in range(abs1.shape[0]):
4 for j in range(abs1.shape[1]):
5 if abs1[i, j] == 255:
6 abs1[i, j] = 0
7mid_img[mid_img.shape[0] - 1] = 0
8mid_img[:, 0] = 0
9mid_img[0] = 0
10mid_img[:, mid_img.shape[1] - 1] = 0
11# 边缘检测
12laplacian = cv2.Laplacian(abs1, cv2.CV_16S, ksize=7)
13dst = cv2.convertScaleAbs(laplacian)
14#cv2.imshow("laplacian", dst)
15# 形态学梯度,膨胀和腐蚀的差别,看上去像轮廓
16kernel = np.ones((7, 7), np.uint8)
17tt = cv2.morphologyEx(abs1, cv2.MORPH_GRADIENT, kernel)
18tt[(tt > 5) & (tt <= 200)] = 255
(6)滑动窗口拟合曲线:
利用边缘检测找出车道信息,然后统计直方图中左右中依次最大为左中右车道。然后创建滑动窗口,数量为50,宽度为20。为了更好地效果,设置寻找最小值为0即可。代码见文末源码。
测试和调试
调试效果图如下可见:
图22 视频训练点击效果图
图23 视频播放按钮点击效果图
源码
有任何问题欢迎评论区留言~完整代码链接如下:
https://pan.baidu.com/s/1I7in6ys2hq9RK7owyUdoFg
提取码:nl6x