一、背景
无人驾驶技术近年来发展迅速。无人车若想实现自动驾驶,从视觉的角度上讲其要先学会观察道路,具体来说,就是检测车道线。包括识别车道线与车的位置关系,是实线还是虚线等。本文将简单介绍车道线检测的基本技术,将在python平台上使用opencv库进行测试。
二、车道线识别主要原理
本车道线识别的算法流程图如图1所示,本部分后面将对其各部分原理进行简要说明。
图1
1)彩色图像转化为灰度图像
通常的彩色图像是三通道的,为处理简便,将彩色图像转化为单通道的灰度图像。转化的经验公式如下。该公式是根据人眼对RGB三种不同颜色的感光强度进行分配的。
Gray =0.299∙R+0.587∙G+0.114∙B
2)图像平滑
图像中噪声太严重将会影响对目标的分割提取,所以前期需要对图像进行平滑滤波,降低图像噪声和减小细节的同时,还能保留边界。图像平滑滤波算法有许多种,如中值滤波、高斯滤波等。本项目选用高斯滤波算法对图像进行去噪,其原理是重新计算图像中每个点的值,计算的时候将该点与周围点进行加权平均,权重符合高斯分布。一般将权重按对应位置组成矩阵形式,称为高斯核,其中本项目将用到的是大小为5的高斯核,如下所示:
3)边缘提取
车道线的一个主要特征是他的边缘比较明显,因此可以使用边缘检测算法将之从原图像中提取出来。边缘检测的结果就是标识出数字图像中亮度值变化明显的点。边缘检测的实质是计算图片中每个点的梯度(像素值的变化情况),变化值越大表明是边缘的可能性越高。本文使用canny算法进行图像边缘提取,其计算梯度时使用了卷积,计算方向[0、45、90、135]度间的梯度值。
计算出每个像素点的梯度值后,接下来需要筛选可疑边缘点。方法是将每个像素点的梯度值与梯度方向上的俩个点进行比较,当且仅当这个点的梯度值最大时才保留,否则舍弃。例如,梯度方向是0度,那么将当前梯度值与左右两边的梯度值进行比较,当且仅当当前梯度值最大才保留。
从前一步筛选出的点只是可疑边缘点,会存在部分误标记的点,这些标记的点可能是由噪声或则颜色的变化造成的。解决思路是设置俩个阈值low_threshold、high_threshold,当像素点梯度值小于low_threshold时,说明该像素点是误标记点,将其丢弃;当像素点梯度值大于high _threshold时,说明该像素点不是误标记点,将其保留;当像素点梯度值介于俩阈值之间时,当且仅当存在其相连点的梯度值大于high _threshold时才保留为边缘点。
4)ROI区域选择
通过Canny算法进行边缘检测获得边缘图像后,边缘图像不仅包括所需要的车道线边缘,同时包括不需要的其他车道以及周围围栏的边缘。去掉这些边缘的办法是确定一个多边形的可视域,只保留可视域部分的边缘信息。其依据是camera相对车是固定的,车相对于车道的相对位置也基本固定,所以车道在camera中基本保持在一个固定区域内。所以可以保留边缘图像中的可视域部分,去除其他无关信息对图像处理的干扰。
5)霍夫线变换
霍夫变换是一种特征提取技术,检测具有特定形状的物体,通常是直线、圆、椭圆等。其原理是将原空间映射到参数空间,在参数空间进行投票获得所需的图像。
霍夫线变换是一种用来寻找直线的方法。一条直线在图像二维空间中由俩个变量表示,一种方法是在笛卡尔坐标系中用(m,b)斜率和截距表示;另一种方法是在极坐标系中用(r,θ)极径和极角表示。
对于霍夫变换,我们将用极坐标系来表示直线。因此,直线的表达式可表示为:,化简得:。一般来说,对于点,我们可以将通过这个点的一族直线统一定义为:。这意味着每一对代表一条通过点的直线。如果对于一个给定的点,我们在极坐标系对极径极角平面绘出所有通过它的直线,将得到一条正弦曲线。若对图像中所有点进行上述操作,如果俩个不同点进行上述操作后得到的曲线在平面极径极角平面相交,这就意味着它们通过同一条直线。霍夫线变换要做的就是追踪图像中每个点对应曲线的交点。如果交于一点的曲线的数量超过了阈值,那么可以认为这个交点所代表的参数对在原图像中为一条直线。最后在根据这个参数对在原图像中绘制出车道线。
三、实现代码以及运行效果分析
1)源代码及注释:
import numpy as np
import cv2
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
'''根据极角极径参数在原图像中画线'''
def draw_lines(img, lines, color=[255, 0, 0], thickness=6):
for line in lines:
for x1,y1,x2,y2 in line:
# 在图像中设置俩条水平线,只有当直线于其相交,才绘制该直线,用于筛选直线
top_horizon_line = ([0, img.shape[0]*0.6], [img.shape[1], img.shape[0]*0.7])
bottom_horizon_line = ([0, img.shape[0]], [img.shape[1], img.shape[0]])
line_intersection_top = line_intersection(top_horizon_line, ([x1, y1], [x2, y2]))
line_intersection_bottom = line_intersection(bottom_horizon_line, ([x1, y1], [x2, y2]))
if line_intersection_top == None or line_intersection_bottom == None:
return
# 绘制直线
cv2.line(img, line_intersection_top, line_intersection_bottom, color, thickness)
'''判断俩条线段是否相交'''
def line_intersection(line1, line2):
xdiff = (line1[0][0] - line1[1][0], line2[0][0] - line2[1][0])
ydiff = (line1[0][1] - line1[1][1], line2[0][1] - line2[1][1])
def det(a, b):
return a[0] * b[1] - a[1] * b[0]
div = det(xdiff, ydiff)
if div == 0:
return None
d = (det(*line1), det(*line2))
x = det(d, xdiff) / div
y = det(d, ydiff) / div
return int(x), int(y)
image = mpimg.imread("./test_images/lane.jpg") # 读取源图像
showImg = True
image = np.array(image)
grayscale_image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) # 转为灰度图
kernel_size = 5
# 高斯平滑,大小为5的高斯核
gaussian_blur_image = cv2.GaussianBlur(grayscale_image, (kernel_size, kernel_size), 0)
# 设置阈值,进行canny边缘提取
canny_low_threshold = 10
canny_high_threshold = 20
edge_image = cv2.Canny(gaussian_blur_image, canny_low_threshold, canny_high_threshold)
# ROI可视区域选择,本程序选取左测公路区域
image_shape = edge_image.shape
x_offset = 200
y_offset = 90
v1 = (0, image_shape[0] - y_offset / 2)
v2 = (int(image_shape[1]/4 + x_offset), int(image_shape[0]/2+y_offset))
v3 = (int(image_shape[1]/2 - x_offset), int(image_shape[0]/2+y_offset))
v4 = (image_shape[1] / 2 + x_offset, image_shape[0] - y_offset / 2)
vert = np.array([[v1, v2, v3, v4]], dtype=np.int32)
mask = np.zeros_like(edge_image)
if len(edge_image.shape) > 2:
channel_count = edge_image.shape[2]
ignore_mask_color = (255,) * channel_count
else:
ignore_mask_color = 255
# ROI可视区填充,在用mask与灰度图进行与运算,即在灰度图中得可视区
cv2.fillPoly(mask, vert, ignore_mask_color)
masked_edge_image = cv2.bitwise_and(edge_image, mask)
# 显示可视区的边缘提取二值图像
cv2.imshow("edge", masked_edge_image)
print(np.array([[v1, v2, v3, v4]], dtype=np.int32))
# 霍夫线变换
rho = 2 # 设置极径分辨率
theta = (np.pi)/180 # 设置极角分辨率
threshold = 100 # 设置检测一条直线所需最少的交点
min_line_len = 300 # 设置线段最小长度
max_line_gap = 20 # 设置线段最近俩点的距离
lines = cv2.HoughLinesP(masked_edge_image, rho, theta, threshold, np.array([]), minLineLength=min_line_len, maxLineGap=max_line_gap)
hough_line_image = np.zeros((masked_edge_image.shape[0], masked_edge_image.shape[1], 3), dtype=np.uint8)
# 绘制检测到的直线
draw_lines(hough_line_image, lines)
# 将直线与原图像合成为一幅图像
sync_image = cv2.addWeighted(image, 0.8, hough_line_image, 1, 0)
# 显示图像
if showImg:
plt.imshow(sync_image)
plt.show()
cv2.imshow("lane", sync_image)
cv2.waitKey(0)
2)运行结果
图2 ROI边缘提取二值图像
图3 车道线识别图像
从图2、图3可以看出,本车道线识别算法能较好的识别出车道线,而且识别速度块。不足的地方在于对较为模糊的车道线不能很好的将其检测出来,比如图3中左边公路中的中间车道线没有检测出来。从图2的ROI边缘提取二值图像中可以看出,这是canny边缘提取的时候中间车道线的边缘断断续续,而且周边较多干扰。
四、总结
车道识别是无人驾驶中的重要技术,虽然本设计用的算法较为简单,而且识别效果也没有达到很好,但这却是学习其他高深复杂算法的基础和过渡。虽然以前就用过canny算法,但之前却没有很好的理解它。通过本本项目的学习,让我对canny算法和霍夫变换有了更深刻的理解。