无人驾驶-OpenCV车道线检测
基于OpenCV的车道线检测技术用于图像处理领域中对道路场景进行分析的任务研究

在图像坐标系中确定左右车道线的直线方程式及其有效范围。通过将左右车道线的数学表达式描绘至原始图像,如图所示。

原始图像
在学习图像处理之前,我们有必要回顾一下初中物理课程中的相关知识——特别是关于光的基本理论。其基本组成单元即为红、绿、蓝三种颜色(分别对应RGB值)。通过调节红、绿、蓝三种颜色的比例组合,我们可以生成丰富的可见色彩。如图所示。

每个像素单元都包含RGB(红绿蓝)三种色素通道的信息。为了便于描述RGB颜色模型,在计算机领域对每个色素通道设定明暗范围为0~255进行了限定范围。当一个像素单元的R色素值达到最大255而G和B值均为0时,则显示为最亮红色;当R、G、B三个色素值均为255时,则呈现最亮白色;当所有三个色素值均为0时,则显示最暗黑色状态;在RGB颜色模型中不存在比[255,255,255]组合更为明亮的颜色表现形式。
基于Python编程语言开发的OpenCV库中实现了一幅彩色图像可以被理解为三个单色通道图像叠加的结果这一重要理论基础。具体而言,在Python中使用OpenCV库读取名为test_img.jpg的图片的代码如下所示:

import cv2
img = cv2.imread('whiteCarLaneSwitch.jpg') #这里要把图片后缀也打上
读取图片后,可以把图片视为一个二维数组,其中每个数组元素存储了三个值,这些值分别对应于RGB三个颜色通道的数值.


按照OpenCV的规定,在数字图像处理中,默认坐标系的原点通常设在左上方位置。其中X轴向右延伸,Y轴则向下延伸,如图所示。

灰度处理

通过调用OpenCV提供的cvtColor()函数(链接),我们可以实现图像的灰度转换
#由于使用cv2.imread()读到的img的数据排列为BGR,因此这里的参数为BGR2GRAY
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
灰度处理后的图像如下图所示:

在图像处理中,通过灰度化后的图像进行边缘检测来增强车道线的对比度。
low_threshold = 40
high_threshold = 150
canny_image = cv2.Canny(gray, low_threshold, high_threshold)

在完成边缘提取后,在图像处理过程中车道线显示效果得到了显著提升。为了实现车内道的车道线检测任务,在处理过程中需要先确定并提取出感兴趣区域。一种常用的方法是通过矩形框选来确定并截取所需区域。在实际操作中,默认会选取一个较大的矩形框选范围作为初始候选区域;随后根据图像特征自动优化这一初始范围以获得最佳匹配结果。遍历该区域内每一个像素的位置信息;对于超出三角形边界范围的像素点,则将其设为不可见(即像素值设为0)。

为了达成图像处理的核心目标,
建议对OpenCV库中的相关功能进行打包整合,
并在此基础上创建一个区域感兴趣(Region of Interest,简称ROI)的功能模块:
def region_od_interest(img,vertices):
# 定义一个和输入图像同样大小的全黑图像mask,这个mask也称掩膜
# 掩膜的介绍,可参考:https://www.cnblogs.com/skyfsm/p/6894685.html
mask = np.zeros_like(img)
# 根据输入图像的通道数,忽略的像素点是多通道的白色,还是单通道的白色
if(len(img.shape) > 2):
channel_count = img.shape[2]# i.e. 3 or 4 depending on your image
ignore_mask_color = (255,) * channel_count
else:
ignore_mask_color = 255
# [vertices]中的点组成了多边形,将在多边形内的mask像素点保留,
cv2.fillPoly(mask,[vertices],ignore_mask_color)
# 与mask做"与"操作,即仅留下多边形部分的图像
masked_image = cv2.bitwise_and(img,mask)
return masked_image
边缘提取后的图像的截取。
#图像像素行数
rows = canny_image.shape[0] #540行
#图像像素列数
cols = canny_image.shape[1]#960列
left_bottom = [0,canny_image.shape[0]]
right_bottom = [canny_image.shape[1],canny_image.shape[0]]
apex = [canny_image.shape[1]/2,310]
vertices = np.array([left_bottom,right_bottom,apex],np.int32)
roi_image = region_od_interest(canny_image,vertices)cv2.imshow('roi_image',roi_image)
【注】几行代码的运行结果:
img = cv2.imread('whiteCarLaneSwitch.jpg')#这里要把图片后缀也打上
print(img.shape)
print(len(img.shape))

#图像像素行数
rows = canny_image.shape[0] #540行
#图像像素列数
cols = canny_image.shape[1]#960列
left_bottom = [0,canny_image.shape[0]]#[0,540]
right_bottom = [canny_image.shape[1],canny_image.shape[0]]#[960,540]
apex = [canny_image.shape[1]/2,310]#[480,310]
vertices = np.array([left_bottom,right_bottom,apex],np.int32)
print(vertices)

截取感兴趣的区域图像如下图所示:

霍夫变换
经过灰度处理、边缘检测、感兴趣区域截取后,我们终于将左右车道线从复杂的图像中提取出来了。接下来,我们使用霍夫变换来提取图像中的直线(段)。
霍夫变换是一种特征检测方法,其原理和推导过程可以参看经典霍夫变换
(Hough Transform)
在图像中使用霍夫变换不仅能够识别图像中的直线,还能识别出图像中的圆、椭圆等特征。OpenCV为我们提供了霍夫变换检测直线的函数,可以通过设置不同的参数,检测不同长度的线段。由于车道线存在虚线的可能,因此线段的检测长度不能设置地太长,否则短线段会被忽略掉。
OpenCV的霍夫变换直线检测函数使用方法如下:
#霍夫变换函数
def Hough_Transform(roi_image):
#OpenCV霍夫变换直线检测函数
rho = 2# distance resolution in pixels of the Hough grid
theta = np.pi / 180 # angular resolution in radians of the Hough grid
threshold = 15 # minimum number of votes (intersections in Hough grid cell)
min_line_length = 40#minimum number of pixels making up a line
max_line_gap = 20# maximum gap in pixels between connectable line segments
# Hough Transform 检测线段,线段两个端点的坐标存在lines中
lines = cv2.HoughLinesP(roi_image,rho,theta,threshold,np.array([]),min_line_length,max_line_gap)
return lines
创建一个绘图函数模块,并设计并实现一段代码用于将线段绘制到图像中;同时构建相应的功能模块以便完成对线段的可视化显示
def draw_lines(img,lines,color,thickness=2):
for line in lines:
for x1,y1,x2,y2 in line:
cv2.line(img,(x1,y1),(x2,y2),color,thickness)
将得到线段绘制在原始图像上
# 将得到线段绘制在原始图像上
line_image = np.copy(img)
draw_lines(line_image,lines,[255,0,0],6)

通过观察右车道线的断开状态, 我们发现其与我们的预期输出结果极为接近. 经过对比实验发现, 霍夫变换所得的线段集合与实际输出结果仍存在显著差异. 为了实现预期效果, 我们必须对数据进行必要的后续处理操作. 具体来说, 需要完成以下两个关键步骤: 第一步, 基于图像坐标系中的斜率信息, 将所有检测到的左、右车道线分别存储于左车道和右车道变量中; 然后对左车道所有点集和右车道所有点集分别进行最小二乘直线拟合, 得到的就是最终的理想直线参数. 第二步, 根据拟合后的直线方程确定左右车道线的上下边缘位置. 这一过程基于一个合理的假设即: 在理想状态下左右车道线呈现平行状态, 因此只需考察所有点的y坐标的极值范围即可确定上下边缘位置. 基于此分析流程, 我们对draw_lines()函数进行了重构并将其核心算法整合至此.
#封装一个绘图函数,实现把线段绘制在图像上的功能,以实现线段的可视
def draw_lines(img,lines,color,thickness=2):
left_lines_x = []
left_lines_y = []
right_lines_x = []
right_lines_y = []
line_y_max = 0
line_y_min = 999
for line in lines:
for x1,y1,x2,y2 in line:
if y1 > line_y_max:
line_y_max = y1
if y2 > line_y_max:
line_y_max = y2
if y1 < line_y_min:
line_y_min = y1
if y2 < line_y_min:
line_y_min = y2
k = (y2 - y1) / (x2 - x1)
if k < -0.3:
left_lines_x.append(x1)
left_lines_y.append(y1)
left_lines_x.append(x2)
left_lines_y.append(y2)
if k > 0.3:
right_lines_x.append(x1)
right_lines_y.append(y1)
right_lines_x.append(x2)
right_lines_y.append(y2)
left_line_k, left_line_b = np.polyfit(left_lines_x, left_lines_y, 1)
right_line_k, right_line_b = np.polyfit(right_lines_x, right_lines_y, 1)
cv2.line(img,
(int((line_y_max - left_line_b)/left_line_k), line_y_max),
(int((line_y_min - left_line_b)/left_line_k), line_y_min),
color, thickness)
cv2.line(img,
(int((line_y_max - right_line_b)/right_line_k), line_y_max),
(int((line_y_min - right_line_b)/right_line_k), line_y_min),
color, thickness)
通过线段的后续处理, 即可获得符合输出要求的两条直线方程及其斜率、截距和有效长度. 可将后处理所得结果叠加至原图中, 如下图所示.

最后贴出完整代码:
import cv2
import numpy as np
def region_od_interest(img,vertices):
# 定义一个和输入图像同样大小的全黑图像mask,这个mask也称掩膜
# 掩膜的介绍,可参考:https://www.cnblogs.com/skyfsm/p/6894685.html
mask = np.zeros_like(img)
# 根据输入图像的通道数,忽略的像素点是多通道的白色,还是单通道的白色
if(len(img.shape) > 2):
channel_count = img.shape[2]# i.e. 3 or 4 depending on your image
ignore_mask_color = (255,) * channel_count
else:
ignore_mask_color = 255
# [vertices]中的点组成了多边形,将在多边形内的mask像素点保留,
cv2.fillPoly(mask,[vertices],ignore_mask_color)
# 与mask做"与"操作,即仅留下多边形部分的图像
masked_image = cv2.bitwise_and(img,mask)
return masked_image
'''
def draw_lines(img,lines,color,thickness=2):
for line in lines:
for x1,y1,x2,y2 in line:
cv2.line(img,(x1,y1),(x2,y2),color,thickness)
'''
#封装一个绘图函数,实现把线段绘制在图像上的功能,以实现线段的可视
def draw_lines(img,lines,color,thickness=2):
left_lines_x = []
left_lines_y = []
right_lines_x = []
right_lines_y = []
line_y_max = 0
line_y_min = 999
for line in lines:
for x1,y1,x2,y2 in line:
if y1 > line_y_max:
line_y_max = y1
if y2 > line_y_max:
line_y_max = y2
if y1 < line_y_min:
line_y_min = y1
if y2 < line_y_min:
line_y_min = y2
k = (y2 - y1) / (x2 - x1)
if k < -0.3:
left_lines_x.append(x1)
left_lines_y.append(y1)
left_lines_x.append(x2)
left_lines_y.append(y2)
if k > 0.3:
right_lines_x.append(x1)
right_lines_y.append(y1)
right_lines_x.append(x2)
right_lines_y.append(y2)
left_line_k, left_line_b = np.polyfit(left_lines_x, left_lines_y, 1)
right_line_k, right_line_b = np.polyfit(right_lines_x, right_lines_y, 1)
cv2.line(img,
(int((line_y_max - left_line_b)/left_line_k), line_y_max),
(int((line_y_min - left_line_b)/left_line_k), line_y_min),
color, thickness)
cv2.line(img,
(int((line_y_max - right_line_b)/right_line_k), line_y_max),
(int((line_y_min - right_line_b)/right_line_k), line_y_min),
color, thickness)
#霍夫变换函数
def Hough_Transform(roi_image):
#OpenCV霍夫变换直线检测函数
rho = 2# distance resolution in pixels of the Hough grid
theta = np.pi / 180 # angular resolution in radians of the Hough grid
threshold = 15 # minimum number of votes (intersections in Hough grid cell)
min_line_length = 40#minimum number of pixels making up a line
max_line_gap = 20# maximum gap in pixels between connectable line segments
# Hough Transform 检测线段,线段两个端点的坐标存在lines中
lines = cv2.HoughLinesP(roi_image,rho,theta,threshold,np.array([]),min_line_length,max_line_gap)
return lines
def main():
img = cv2.imread('whiteCarLaneSwitch.jpg')#这里要把图片后缀也打上
print(img.shape)
print(len(img.shape))
#print(img)
#由于使用cv2.imread()读到的img的数据排列为BGR,因此这里的参数为BGR2GRAY
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
low_threshold = 40
high_threshold = 150
canny_image = cv2.Canny(gray, low_threshold, high_threshold)
cv2.imshow('canny_image',canny_image)
#图像像素行数
rows = canny_image.shape[0] #540行
#图像像素列数
cols = canny_image.shape[1]#960列
left_bottom = [0,canny_image.shape[0]]#[0,540]
right_bottom = [canny_image.shape[1],canny_image.shape[0]]#[960,540]
apex = [canny_image.shape[1]/2,310]#[480,310]
vertices = np.array([left_bottom,right_bottom,apex],np.int32)
print(vertices)
roi_image = region_od_interest(canny_image,vertices)
cv2.imshow('roi_image',roi_image)
lines = Hough_Transform(roi_image)
# 将得到线段绘制在原始图像上
line_image = np.copy(img)
draw_lines(line_image,lines,[255,0,0],6)
cv2.imwrite('D:\PythonProgram\OpenCV\OpenCV_Lane line_Detection\line_image.jpg',line_image)
cv2.imshow('src',line_image)
cv2.waitKey(0)
if __name__ == '__main__':
main()
