无人驾驶之车道线检测(一)
车道检测(Advanced Lane Finding Project)
在无人驾驶技术领域中,车道检测被视为一项基础技术,在这项核心技术上值得深入研究。为了系统地阐述这一功能特性,在本系列文章中我计划分两部分展开讨论:第一篇将采用传统算法来实现车道检测的技术方案;后续文章则采用深度学习方法展开探讨

占坑。。。。最近比较忙后续更新
先看几个流程图:


实现步骤:
- 通过提供的棋盘格图片集合计算相机的内参数矩阵和畸变系数。
- 对图像进行校正以获取清晰的车道线捕获二进制图像。
- 通过设置梯度阈值和颜色阈值等处理方法得到一个清晰的二进制图像。
- 对二进制图像应用透视变换获得鸟瞰图视图。
- 针对不同颜色特征、光照条件以及清晰度差异的情况,在不同颜色空间中设定相应的梯度阈值和颜色阈值进行处理,并将各处理结果融合生成完整的二进制车道线掩膜。
- 提取生成的二进制掩膜中标注为车道线区域的所有像素点坐标信息。
- 通过对提取到的像素点进行直方图分析确定左右车道线起始点位置并完成曲线拟合运算。
- 分别采用二次多项式模型拟合左右车道线的所有像素点坐标信息(对于噪声较大的像素点区域可实施滤波处理或者应用随机采样一致性算法实现曲线拟合)。
- 计算所得车道线曲线半径及其车辆相对于车道中心偏移量信息并完成显示效果验证(包括可行域边界显示以及曲率与偏移位置数值显示)
摄像机标定
或许大家或多或少都听说过鱼眼相机?其中最常见的鱼眼相机是一种辅助驾驶员倒车的后向摄像头。此外,也有许多摄影爱好者热衷于使用这种镜头拍摄图像,在实践中往往能够呈现出令人惊艳的大片效果(如图所示)。

图片来源:优达学城(Udacity)无人驾驶工程师课程
采用鱼眼相机进行成像虽然高端大气上但存在一个显著的问题——畸变(Distortion)从图中可以看到走道上的栏杆应当是笔直延伸至远方然而在成像中却呈现出弯曲的效果这种现象被称为图像畸变这种现象将导致影像失真
使用车载摄像头捕捉的画面中,并不像鱼眼摄像头那样显著地存在畸变现象, 但这种微小的扭曲依然是不可避免的, 因为人眼无法察觉这种细微变化. 当带有轻微畸变的图像被用于车道线检测时, 检测系统的准确性将受到直接影响, 因此第一步工作就是去除这种扭曲现象.
为了解决 车载摄像机图像的畸变问题 ,摄像机标定技术 应运而生。
相机标定的过程包括对已知物体拍照,并通过计算该物体在实际空间中的位置与在图像中的位置之间的偏差值(即畸变参数),从而利用这些偏差值来校正其他受扭曲的图像的技术。
通常情况下 可以选择各种已知形状用于摄像机校准 但业内通行的做法是依赖于棋盘格 这是因为棋盘格图案具有规律性且对比度高 便于自动化检测各个棋盘格交点 从而非常适合用于相机标定工作 如下图所示为典型的10×7(7行10列)棋盘格结构

OpenCV库支持用于相机标定的功能模块,并包含能够自动识别棋盘格交点(黑白交替的小方块)的函数cv2.findChessboardCorners() 。该功能仅需输入完整棋盘格图像以及横向和纵向交点的数量即可完成配置。一旦完成标定过程后,在后续开发中我们可以通过调用函数cv2.drawChessboardCorners() 来显示标定结果图。
棋盘格原图如下所示:

图片来源位置:https://github.com/udacity/CarND-Advanced-Lane-Lines/blob/master/camera_cal/calibration2.jpg
使用OpenCV自动交点检测的结果如下:

通过计算交点检测结果后调用该函数即可获得相机的畸变参数
调用cv2.calibrateCamera()函数完成标定工作;该函数将输出标定结果信息,并返回相机的内参数矩阵、畸变系数以及旋转矩阵和平移向量。
为了实现摄像机标定所得出的畸变系数更加精确的目标,我们采用车载摄像机在不同视角下拍摄20幅棋盘格图像,并记录所有交叉点的位置信息,在整个计算过程中综合考虑这些数据以提高畸变系数的准确性
我们将对图像文件进行读取,并对其进行预处理步骤;然后计算图像中的交叉点位置;最后确定相机参数的一系列操作流程封装成一个函数。
1.计算摄像机畸变系数
#################################################################
# Step 1 : Calculate camera distortion coefficients
#################################################################
def getCameraCalibrationCoefficients(chessboardname, nx, ny):
# prepare object points, like (0,0,0), (1,0,0), (2,0,0) ....,(6,5,0)
objp = np.zeros((ny * nx, 3), np.float32)
objp[:,:2] = np.mgrid[0:nx, 0:ny].T.reshape(-1,2)
# Arrays to store object points and image points from all the images.
objpoints = [] # 3d points in real world space
imgpoints = [] # 2d points in image plane.
images = glob.glob(chessboardname)
if len(images) > 0:
print("images num for calibration : ", len(images))
else:
print("No image for calibration.")
return
ret_count = 0
for idx, fname in enumerate(images):
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
img_size = (img.shape[1], img.shape[0])
# Finde the chessboard corners
ret, corners = cv2.findChessboardCorners(gray, (nx, ny), None)
# If found, add object points, image points
if ret == True:
ret_count += 1
objpoints.append(objp)
imgpoints.append(corners)
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints,
img_size, None, None)
print('Do calibration successfully')
return ret, mtx, dist, rvecs, tvecs
代码解读
调用之前封装好的函数,获取畸变参数。
nx = 9
ny = 6
ret, mtx, dist, rvecs, tvecs = getCameraCalibrationCoefficients('camera_cal/calibration*.jpg', nx, ny)
代码解读
随后调用OpenCV提供的函数cv2.undistort()并输入刚刚计算得到的畸变参数,则可以对畸变的图像进行畸变修正处理
#################################################################
# Step 2 : Undistort image
#################################################################
def undistortImage(distortImage, mtx, dist):
return cv2.undistort(distortImage, mtx, dist, None, mtx)
代码解读
以畸变的棋盘格图像为例,进行畸变修正处理
# Read distorted chessboard image
test_distort_image = cv2.imread('./camera_cal/calibration4.jpg')
# Do undistortion
test_undistort_image = undistortImage(test_distort_image, mtx, dist)
代码解读
筛选图像
通过分析输入视频的数据可以看出,在行驶过程中车辆会遇到路面不平的情况、车道线难以辨认的情形以及明显的路面颜色变化,并且障碍物旁边的阴影会影响检测效果等多方面的挑战性环境。为了提高检测效率和准确性,在开发相关算法时应特别关注并筛选出这些具有典型特征的复杂场景,并在此基础上优化算法参数设置以实现对车道线边界位置的有效识别
按照以下代码流程获取视频中的图像数据,并经过畸变修正处理过程后生成调整后的图像数据;随后创建指定名称为original_image的文件夹,并将处理后的图像数据存储其中;以便后续筛选使用。
video_input = 'project_video.mp4'
cap = cv2.VideoCapture(video_input)
count = 1
while(True):
ret, image = cap.read()
if ret:
undistort_image = undistortImage(image, mtx, dist)
cv2.imwrite('original_image/' + str(count) + '.jpg', undistort_image)
count += 1
else:
break
cap.release()
代码解读
在original_image文件夹内,从该文件夹中选择以下6个场景:包括常见正常的直线和曲线行驶环境(straight and curved road scenarios),以及复杂的阴影区域和明暗对比强烈的环境(shadows and high contrast areas)等具有挑战性的测试条件(challenging test conditions)。假设后续开发的先进车道识别系统能够完美应对这六种情况,则将该算法应用于视频数据后也会实现精准的道路车道识别效果。本文主要采用传统图像处理方法来实现道路车道检测,并依赖于高度定义化的特征提取和启发式算法(highly defined features and heuristic approaches)。通常会依赖后续处理技术(post-processing techniques),这可能导致较大的计算负担(resulting in significant computational overhead)在这种多变的道路环境下难以有效扩展应用能力。
透视变换
在完成图像的畸变校正后, 就要将注意力转移到车道线. 与之前的技术类似, 这里需要设定一个感兴趣的区域. 显然, 在我们的场景中这个 lane 是车辆前方的道路. 为了获取感兴趣区域, 在我们的场景中我们只需要对车辆前方的道路应用透视变换即可.
在完成图像的畸变校正后, 就要将注意力转移到车道线. 与之前的技术类似, 这里需要设定一个感兴趣的区域. 显然, 在我们的场景中这个 lane 是车辆前方的道路. 为了获取感兴趣区域, 在我们的场景中我们只需要对车辆前方的道路应用透视变换即可.
在图像形成过程中,在被观察物体离摄像机越远的情况下,在视觉效果上显得尺寸逐渐缩小的一种现象称为‘透视’。真实世界中平行延伸的道路边缘线条会在图像最远处出现交汇点这一现象的发生原因即为‘透视成像’原理所致
以直立于路边的交通标志牌为例,在摄像头通常情况下获取的照片中其成像结果一般如下图所示

在这一幅图像中,在本来应该是正八边形的标志牌上呈现出了歪斜不齐的八边形形状。
借助射影转换方法能够将不规则的八边形转换为正八边形;应用该方法后的实例如下:

透视变换的基本原理是:随后创建一个与左图尺寸一致的新图像,并在该图像中识别并标记图像中位于左右两侧的四个特征点(如图所示)。为了实现这一目标,请先获取这些关键位置的具体坐标值并将它们存储在一个变量中命名为src_points。需要注意的是,在处理过程中所选取的关键点必须形成一个平行四边形形状以确保变换的有效性
基于先验知识可知,在左图中的平行四边形区域,在现实世界中表现为矩形。因此,在右边的图像中合理地确定位置后选取适当的位置并合理地确定一个矩形区域使其四个顶点与原图中的src_points一一对应我们将其定义为dst_points。
通过获取src_points和dst_points后,我们即可完成相应的投影变换。
使用OpenCV库实现透视变换的代码如下:
#################################################################
# Step 3 : Warp image based on src_points and dst_points
#################################################################
# The type of src_points & dst_points should be like
# np.float32([ [0,0], [100,200], [200, 300], [300,400]])
def warpImage(image, src_points, dst_points):
image_size = (image.shape[1], image.shape[0])
# rows = img.shape[0] 720
# cols = img.shape[1] 1280
M = cv2.getPerspectiveTransform(src, dst)
Minv = cv2.getPerspectiveTransform(dst, src)
warped_image = cv2.warpPerspective(image, M,image_size, flags=cv2.INTER_LINEAR)
return warped_image, M, Minv
代码解读
同理,在对经过畸变校正处理的道路图像中,我们同样采用同样的方法来实现我们感兴趣区域的透视变换。
如下图所示,在直线行驶的道路图像中,在左右车道线边缘选择一个梯形区域。该区域在真实道路上应当呈现为长方形形状。因此我们将该梯形区域进行投影变换使其成为长方形进而将其在右图横坐标适当地位置设置为长方形的四个端点位置最终实现的效果类似于'俯视图'的效果

调用给定的代码库,并且通过持续地修改src参数和dst目标区域的值进行优化,在直线行驶路线中能够生成理想的透视变换图像。
test_distort_image = cv2.imread('test_images/test4.jpg')
# 畸变修正
test_undistort_image = undistortImage(test_distort_image, mtx, dist)
# 左图梯形区域的四个端点
src = np.float32([[580, 460], [700, 460], [1096, 720], [200, 720]])
# 右图矩形区域的四个端点
dst = np.float32([[300, 0], [950, 0], [950, 720], [300, 720])
test_warp_image, M, Minv = warpImage(test_undistort_image, src, dst)
代码解读
最终阶段,在经过精心筛选后得到的6幅图像上,我们将全部采用优化后的src和dst参数进行透视转换处理。这个过程确实耗费了不少精力,请多包涵!
提取车道线
在《无人驾驶技术入门(十四): 初识图像之初级车道线检测》一文中
需要注意的是,在使用Canny边缘提取算法时,该算法会识别并提取图像中不同方向和明暗变化的位置所形成的边缘区域。值得注意的是,在处理存在树木阴影的道路图像时,该算法可能会将树影轮廓也纳入边缘检测结果之中,这种情况我们希望避免发生
因此我们采用了Sobel边缘提取算法这一方法。相较于Canny而言,Sobel的优势在于能够支持横向或纵向边缘的选择。通过分析投影变换后的图像,我们可以观察到车道线在横向方向上的显著变化。
对OpenCV中的cv2.Sobel()功能进行封装后,在完成对边缘提取后得到的图像进行二进制图的转换操作中,在边缘检测成功的像素点标记为白色(值设为1),未检测到边缘的像素则标记为黑色(值设为0)。
def absSobelThreshold(img, orient='x', thresh_min=30, thresh_max=100):
# Convert to grayscale
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
# Apply x or y gradient with the OpenCV Sobel() function
# and take the absolute value
if orient == 'x':
abs_sobel = np.absolute(cv2.Sobel(gray, cv2.CV_64F, 1, 0))
if orient == 'y':
abs_sobel = np.absolute(cv2.Sobel(gray, cv2.CV_64F, 0, 1))
# Rescale back to 8 bit integer
scaled_sobel = np.uint8(255*abs_sobel/np.max(abs_sobel))
# Create a copy and apply the threshold
binary_output = np.zeros_like(scaled_sobel)
# Here I'm using inclusive (>=, <=) thresholds, but exclusive is ok too
binary_output[(scaled_sobel >= thresh_min) & (scaled_sobel <= thresh_max)] = 1
# Return the result
return binary_output
代码解读
横向的Sobel边缘提取算法在处理路面阴影与明暗交替变化的道路工况时存在局限性
无法采用基于边缘检测的技术来识别车道线边界后,我们转而聚焦于色彩空间分析
在以上6个场景中,在尽管路面明暗交替的同时(即),偶尔也会有阴影覆盖的情况下(即),然而黄色 和白色 的车道线是一直都存在的。因此(因为),我们可以通过将图中的黄色和白色分割开来,并将其两种颜色整合到同一幅图上来获得较为理想的效果。
一幅图像除了通过RGB三个颜色通道来表示之外,还可以采用HSL和Lab两种模型来进行描述。其中三个通道数值对应的真实色彩分布如图所示。

基于HSL色彩空间的L通道可以提取图像中的白色车道线;同时利用Lab颜色空间的b通道可以提取图像中的黄色车道线;接着将两次提取的结果进行融合并叠加在同一幅图中;这样就能获得两条完整的车道线条。
采用OpenCV提供的cv2.cvtColor()接口来进行图像颜色空间转换操作,在将输入图像中的RGB颜色通道转换为HLS颜色空间后,并对该图像中的L通道进行二值化处理以增强细节信息;接着通过分析白度较高的区域来识别并提取出 lane markings.
通过OpenCV库中的cv2.cvtColor函数实现基于RGB通道的颜色图像转换为Lab颜色空间,并对该图像中的b通道执行分割处理以识别并提取图像中的黄色车道线
通过上述实验可以看出,在图像处理中利用L通道可以有效地分离出白色的车道线,在利用b通道同样可以在图像中清晰地区分出黄色的车道线。即便是在复杂的环境条件下比如树木阴影或者路面颜色发生突变的情况下这些方法依然能最大限度减少引入的噪声干扰
最后,我们使用以下代码,将两个通道分割的图像合并。
hlsL_binary = hlsLSelect(test_warp_image)
labB_binary = labBSelect(test_warp_image)
combined_binary = np.zeros_like(hlsL_binary)
combined_binary[(hlsL_binary == 1) | (labB_binary == 1)] = 1
代码解读
此乃车道线提取的一种方法。除此之外,则有利用HSL与Lab颜色通道配合使用的该规则方法可实现车道线的分割;另外一种则基于深度学习体系构建模型也可完成此目标。其共同目标即在于准确分离出 lane lines within images.
车道线检测
在处理车道线检测任务时,在确定具体位置之前必须先对车道线的位置进行初步判断。为了便于理解,在此我们引入了一个重要的术语——直方图。
以下面这幅包含噪点的图像为例,进行直方图的介绍

我们知道处理的图像分辨率是1280×720像素即72行与128列如果统计每一行中的白色像素数量则会获得128个数值将这些数值绘制到一个坐标系统中横轴标记为从左到右各列编号纵轴表示对应列中的白色像素数目这样就形成了一个直方图如图所示

图片出处:优达学城(Udacity)无人驾驶工程师学位
将两幅图叠加,效果如下:

图片出处:优达学城(Udacity)无人驾驶工程师学位
确定直方图左边区域的最大峰值所在的列号作为左车道线的大致位置;确定直方图右边区域的最大峰值所在的列号作为右车道线的大致位置。
基于直方图确定左右车道线的大致位置的代码如下,在其中leftx_base和rightx_base即代表左右车道线所在列的大致位置
跟踪车道线
视频数据由连续拍摄的画面组成,在前后相邻画面中假设车道线位置保持不变的前提下(即基于持续性假设),可以通过上一帧检测获得的车道线信息作为下一帧图像处理过程中的输入依据,在前一帧图像中围绕已知车道线位置进行局部搜索。这不仅能够降低计算复杂度,并且能够确保检测出稳定的车道线位置(如图所示)。

图中显示的是前一帧检测所得的白色虚线位置;同时,在这些虚线上方形成了一个绿色阴影区域;通过在该阴影区域内搜索白色特征点坐标的方式,则能够快速确定当前帧中的左右车道线候选点。
最终视频处理如下:
nx = 9
ny = 6
ret, mtx, dist, rvecs, tvecs = getCameraCalibrationCoefficients('camera_cal/calibration*.jpg', nx, ny)
src = np.float32([[580, 460], [700, 460], [1096, 720], [200, 720]])
dst = np.float32([[300, 0], [950, 0], [950, 720], [300, 720]])
video_input = 'project_video.mp4'
video_output = 'result_video.mp4'
cap = cv2.VideoCapture(video_input)
fourcc = cv2.VideoWriter_fourcc(*'XVID')
out = cv2.VideoWriter(video_output, fourcc, 20.0, (1280, 720))
detected = False
while(True):
ret, image = cap.read()
if ret:
undistort_image = undistortImage(image, mtx, dist)
warp_image, M, Minv = warpImage(undistort_image, src, dst)
hlsL_binary = hlsLSelect(warp_image)
labB_binary = labBSelect(warp_image, (205, 255))
combined_binary = np.zeros_like(sx_binary)
combined_binary[(hlsL_binary == 1) | (labB_binary == 0)] = 1
left_fit = []
right_fit = []
ploty = []
if detected == False:
out_img, left_fit, right_fit, ploty = fit_polynomial(combined_binary, nwindows=9, margin=80, minpix=40)
if (len(left_fit) > 0 & len(right_fit) > 0) :
detected = True
else :
detected = False
else:
track_result, left_fit, right_fit, ploty, = search_around_poly(combined_binary, left_fit, right_fit)
if (len(left_fit) > 0 & len(right_fit) > 0) :
detected = True
else :
detected = False
result = drawing(undistort_image, combined_binary, warp_image, left_fitx, right_fitx)
out.write(result)
else:
break
cap.release()
out.release()
代码解读
每个相机都必须重新标定其畸变参数。简单更换视频并不能直接应用这套代码中的参数设置。应用效果同样良好,则是因为中国高速公路的整体路况优于这段视频所呈现的情形。
注:其中大部分参考知乎大神陈光的文章。如有转载,请注明出处;侵权将被删除。
其他
车辆检测功能可参考这里
更多资料详见这里:车道线检测最全资料集锦
还有这里:基于摄像头的车道线检测方法一览
参考文献:
1.<>
2.<>
3.<>
4.https://zhuanlan.zhihu.com/p/54866418
5.https://zhuanlan.zhihu.com/c_147309339
6.https://github.com/yang1688899/CarND-Advanced-Lane-Lines
7.https://wenku.baidu.com/view/69dbf2a0f61fb7360b4c6577.html
