无人驾驶--车道线检测实战(附源码)
该项目开发了一种基于计算机视觉的lane detection 技术,在直道和匝道上实时检测车辆与车道线的位置关系并计算出曲线半径及偏离中心的距离。通过棋盘格方法实现了相机校正(包括畸变纠正和平面检测),并结合透视变换将道路图像转换为鸟瞰图以便后续分析;采用多通道颜色空间处理(HLS通道+梯度阈值)提取 lane lines;通过滑动窗口法对pixel坐标进行二次多项式拟合并计算出curvature 和 vehicle position relative to lane center;最终将检测结果还原到原图像并标注曲线半径及偏离中心距离信息(如图所示)。
完成项目后撰写了一份技术总结, 既作为个人复习资料存档, 也希望能帮助他人更好地理解和学习。
打算创建一个专注于无人驾驶领域的微信交流群, 欢迎感兴趣的朋友加入我的微信: wxl609278502, 群聊中请注明: 姓名-单位/学校


测试地点:南京
软硬件要求
- 通用型车载记录仪(允许轻微畸变;建议对存在较大畸变的设备执行图像校正)
- 配备 GPU 的工控机更加理想化;即使无 GPU 也同样适用
- 运行在 Ubuntu 16.04 操作系统上,并搭配 OpenCV 3.2 库使用
实现步骤
- 图像校正(针对相机存在明显的几何畸变情况)
- 截取目标区域(仅对包含车道线信息的目标区域执行处理)
- 采用几何变换方法(将感兴趣区域图片转换为俯视图坐标系)
- 根据不同的光照条件、清晰度和颜色特性设置相应的梯度阈值和颜色阈值,并对每种情况进行单独处理。
- 解析二进制图像以确定属于车道线的像素点位置
- 统计左右两侧峰值点作为起始坐标并作为初始拟合基准
- 通过二次曲线拟合算法建立模型,并对噪声点进行滤波或采用RANSAC算法去除异常数据以提高拟合精度
- 计算出车道曲线半径参数并评估车辆相对于车道中心的位置偏移量
- 展示可行域边界并结合曲率数值输出车辆偏移状态信息
此处将利用OpenCV所提供的技术基于棋盘格图像组来计算相机标定矩阵以及成像参数。首先需要确定棋盘格内角点在世界坐标系中的位置,并与对应的图像坐标进行匹配。由于相机通常处于固定位置且与目标物平行放置,因此假设棋盘格位于XY平面(Z轴为零),其内角点的世界坐标系位置已知。对于每张图片而言,在相同的全局空间中具有相同的物体点集合。通过调用OpenCV函数cv2.findChessboardCorners()并传递灰度图像以及各向异性系数等参数信息即可获取相应的图像角点坐标集
def get_obj_img_points(images,grid=(9,6)):
object_points=[]
img_points = []
for img in images:
#生成object points
object_point = np.zeros( (grid[0]*grid[1],3),np.float32 )
object_point[:,:2]= np.mgrid[0:grid[0],0:grid[1]].T.reshape(-1,2)
#得到灰度图片
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
#得到图片的image points
ret, corners = cv2.findChessboardCorners(gray, grid, None)
if ret:
object_points.append(object_point)
img_points.append(corners)
return object_points,img_points
然后应用上所述的方法获取object_points和img_points后将它们输入到cv2.calibrateCamera()方法中从而能够计算出相机校正矩阵(camera calibration matrix)以及内参数(distortion coefficients)。接着进一步地应用cv2.undistort()算法即可获得校正后的图像。
def cal_undistort(img, objpoints, imgpoints):
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, img.shape[1::-1], None, None)
dst = cv2.undistort(img, mtx, dist, None, mtx)
return dst
以下为其中一张棋盘格图片校正前后对比:

校正图片测试
#获取棋盘格图片
cal_imgs = utils.get_images_by_dir('camera_cal')
#计算object_points,img_points
object_points,img_points = utils.calibrate(cal_imgs,grid=(9,6))
#获取测试图片
test_imgs = utils.get_images_by_dir('test_images')
#校正测试图片
undistorted = []
for img in test_imgs:
img = utils.cal_undistort(img,object_points,img_points)
undistorted.append(img)
在该操作中,采用OpenCV的 getPerspectiveTransform()函数计算变形矩阵,并将原始图像转换为鸟瞰图形式。
def get_M_Minv():
src = np.float32([[(203, 720), (585, 460), (695, 460), (1127, 720)]])
dst = np.float32([[(320, 720), (320, 0), (960, 0), (960, 720)]])
M = cv2.getPerspectiveTransform(src, dst)
Minv = cv2.getPerspectiveTransform(dst,src)
return M,Minv
源起点(starting points)和目标位置(destination positions)应基于实际图像确定。
然后使用”cv2.warpPerspective()”传入相关值获得变形图片(wrapped image)
thresholded_wraped = cv2.warpPerspective(thresholded, M, img.shape[1::-1], flags=cv2.INTER_LINEAR)


该系统采用梯度阈值(Gradient Threshold)和颜色阈值(Color Threshold)等技术手段对校正后的图像进行处理。该方法能够以确定车道线所在位置的像素点为目标完成图像校正工作。系统分别采用一种基于颜色的空间处理方法以及一种基于梯度的空间处理方法以实现复杂的图像校正效果。完整的处理方式可参考源码实现。
下面为使用hls颜色空间的s通道进行阈值过滤:
def hls_select(img,channel='s',thresh=(0, 255)):
hls = cv2.cvtColor(img, cv2.COLOR_RGB2HLS)
if channel=='h':
channel = hls[:,:,0]
elif channel=='l':
channel=hls[:,:,1]
else:
channel=hls[:,:,2]
binary_output = np.zeros_like(channel)
binary_output[(channel > thresh[0]) & (channel <= thresh[1])] = 1
return binary_output
s_thresh = utils.hls_select(img,channel='s',thresh=(180, 255))

在路面较浅且车道线呈黄色的区域中,在此场景下 lane markings 依然保持清晰度。相比之下,在其他区域的表现则不尽如人意(图4、图8)。
因此为有效适应复杂多变的道路环境 建议综合运用多种阈值过滤策略
def abs_sobel_thresh(img, orient='x', thresh_min=0, thresh_max=255):
#装换为灰度图片
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
#使用cv2.Sobel()计算计算x方向或y方向的导数
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))
#阈值过滤
scaled_sobel = np.uint8(255*abs_sobel/np.max(abs_sobel))
binary_output = np.zeros_like(scaled_sobel)
binary_output[(scaled_sobel >= thresh_min) & (scaled_sobel <= thresh_max)] = 1
return binary_output
经过测试分析表明,在35至100范围内的x轴方向阈值进行过滤处理后生成的二进制图像能够有效识别出较为清晰的车道边缘
x_thresh = utils.abs_sobel_thresh(img, orient='x', thresh_min=35, thresh_max=100)

获取左、右车道线的像素坐标
在对二进制图像进行处理时,由于存在一定数量的噪声像素(即干扰或异常数据),为了确保准确识别出 lane 边缘的位置和形状(即 lane boundary detection),需要首先识别出哪些 pixel 属于 lane 线区域。
首先要确定车道线的起始位置(位于图片底部中心位置)。因为 lane markings 区域在 x 轴上呈现明显的聚集范围,在此区域内进行分析会更加高效可靠。
由此可知, 将 images 分为左右两部分, 可以明显地观察到 lane markings 在 x 轴上的变化趋势。
需要注意的是, 左右两边区域在 x 轴上的 pixels 值变化明显地指向了 lane markings 的起始位置。
以下为测试片x轴的像素分布图:

车道像素点拟合
确定左右车道线的基准点后,通过二次曲线模型对车道边缘进行拟合计算。具体实施时,采用宽度为200px的连续9个滑动窗口进行单条车道线像素定位。
def find_line(binary_warped):
# Take a histogram of the bottom half of the image
histogram = np.sum(binary_warped[binary_warped.shape[0]//2:,:], axis=0)
# Find the peak of the left and right halves of the histogram
# These will be the starting point for the left and right lines
midpoint = np.int(histogram.shape[0]/2)
leftx_base = np.argmax(histogram[:midpoint])
rightx_base = np.argmax(histogram[midpoint:]) + midpoint
# Choose the number of sliding windows
nwindows = 9
# Set height of windows
window_height = np.int(binary_warped.shape[0]/nwindows)
# Identify the x and y positions of all nonzero pixels in the image
nonzero = binary_warped.nonzero()
nonzeroy = np.array(nonzero[0])
nonzerox = np.array(nonzero[1])
# Current positions to be updated for each window
leftx_current = leftx_base
rightx_current = rightx_base
# Set the width of the windows +/- margin
margin = 100
# Set minimum number of pixels found to recenter window
minpix = 50
# Create empty lists to receive left and right lane pixel indices
left_lane_inds = []
right_lane_inds = []
# Step through the windows one by one
for window in range(nwindows):
# Identify window boundaries in x and y (and right and left)
win_y_low = binary_warped.shape[0] - (window+1)*window_height
win_y_high = binary_warped.shape[0] - window*window_height
win_xleft_low = leftx_current - margin
win_xleft_high = leftx_current + margin
win_xright_low = rightx_current - margin
win_xright_high = rightx_current + margin
# Identify the nonzero pixels in x and y within the window
good_left_inds = ((nonzeroy >= win_y_low) & (nonzeroy < win_y_high) &
(nonzerox >= win_xleft_low) & (nonzerox < win_xleft_high)).nonzero()[0]
good_right_inds = ((nonzeroy >= win_y_low) & (nonzeroy < win_y_high) &
(nonzerox >= win_xright_low) & (nonzerox < win_xright_high)).nonzero()[0]
# Append these indices to the lists
left_lane_inds.append(good_left_inds)
right_lane_inds.append(good_right_inds)
# If you found > minpix pixels, recenter next window on their mean position
if len(good_left_inds) > minpix:
leftx_current = np.int(np.mean(nonzerox[good_left_inds]))
if len(good_right_inds) > minpix:
rightx_current = np.int(np.mean(nonzerox[good_right_inds]))
# Concatenate the arrays of indices
left_lane_inds = np.concatenate(left_lane_inds)
right_lane_inds = np.concatenate(right_lane_inds)
# Extract left and right line pixel positions
leftx = nonzerox[left_lane_inds]
lefty = nonzeroy[left_lane_inds]
rightx = nonzerox[right_lane_inds]
righty = nonzeroy[right_lane_inds]
# Fit a second order polynomial to each
left_fit = np.polyfit(lefty, leftx, 2)
right_fit = np.polyfit(righty, rightx, 2)
return left_fit, right_fit, left_lane_inds, right_lane_inds
基于滑动窗多项式拟合(sliding window polynomial fitting)所得结果如下:

曲率计算及车道偏离中心计算
def calculate_curv_and_pos(binary_warped,left_fit, right_fit):
# Define y-value where we want radius of curvature
ploty = np.linspace(0, binary_warped.shape[0]-1, binary_warped.shape[0] )
leftx = left_fit[0]*ploty**2 + left_fit[1]*ploty + left_fit[2]
rightx = right_fit[0]*ploty**2 + right_fit[1]*ploty + right_fit[2]
# Define conversions in x and y from pixels space to meters
ym_per_pix = 30/720 # meters per pixel in y dimension
xm_per_pix = 3.7/700 # meters per pixel in x dimension
y_eval = np.max(ploty)
# Fit new polynomials to x,y in world space
left_fit_cr = np.polyfit(ploty*ym_per_pix, leftx*xm_per_pix, 2)
right_fit_cr = np.polyfit(ploty*ym_per_pix, rightx*xm_per_pix, 2)
# Calculate the new radii of curvature
left_curverad = ((1 + (2*left_fit_cr[0]*y_eval*ym_per_pix + left_fit_cr[1])**2)**1.5) / np.absolute(2*left_fit_cr[0])
right_curverad = ((1 + (2*right_fit_cr[0]*y_eval*ym_per_pix + right_fit_cr[1])**2)**1.5) / np.absolute(2*right_fit_cr[0])
curvature = ((left_curverad + right_curverad) / 2)
#print(curvature)
lane_width = np.absolute(leftx[719] - rightx[719])
lane_xm_per_pix = 3.7 / lane_width
veh_pos = (((leftx[719] + rightx[719]) * lane_xm_per_pix) / 2.)
cen_pos = ((binary_warped.shape[1] * lane_xm_per_pix) / 2.)
distance_from_center = veh_pos - cen_pos
return curvature,distance_from_center
通过逆变形矩阵将鸟瞰二进制图检测得到的车道信息融合到原始图像中,并以突出显示的方式标示出这些车道区域。
def draw_area(undist,binary_warped,Minv,left_fit, right_fit):
# Generate x and y values for plotting
ploty = np.linspace(0, binary_warped.shape[0]-1, binary_warped.shape[0] )
left_fitx = left_fit[0]*ploty**2 + left_fit[1]*ploty + left_fit[2]
right_fitx = right_fit[0]*ploty**2 + right_fit[1]*ploty + right_fit[2]
# Create an image to draw the lines on
warp_zero = np.zeros_like(binary_warped).astype(np.uint8)
color_warp = np.dstack((warp_zero, warp_zero, warp_zero))
# Recast the x and y points into usable format for cv2.fillPoly()
pts_left = np.array([np.transpose(np.vstack([left_fitx, ploty]))])
pts_right = np.array([np.flipud(np.transpose(np.vstack([right_fitx, ploty])))])
pts = np.hstack((pts_left, pts_right))
# Draw the lane onto the warped blank image
cv2.fillPoly(color_warp, np.int_([pts]), (0,255, 0))
# Warp the blank back to original image space using inverse perspective matrix (Minv)
newwarp = cv2.warpPerspective(color_warp, Minv, (undist.shape[1], undist.shape[0]))
# Combine the result with the original image
result = cv2.addWeighted(undist, 1, newwarp, 0.3, 0)
return result
采用cv2.putText()方法对原始图像进行处理以显示车道的曲率以及车辆相对于车道中心的位置信息。
def draw_values(img, curvature, distance_from_center):
font = cv2.FONT_HERSHEY_SIMPLEX
radius_text = "Radius of Curvature: %sm" % (round(curvature))
if distance_from_center > 0:
pos_flag = 'right'
else:
pos_flag = 'left'
cv2.putText(img, radius_text, (100, 100), font, 1, (255, 255, 255), 2)
center_text = "Vehicle is %.3fm %s of center" % (abs(distance_from_center), pos_flag)
cv2.putText(img, center_text, (100, 150), font, 1, (255, 255, 255), 2)
return img
该代码基于 Udacity 的开源项目开发:该开源项目可通过以下地址访问:https://github.com/yang1688899/CarND-Advanced-Lane-Lines
