Advertisement

无人驾驶--车道线检测实战(附源码)

阅读量:

该项目开发了一种基于计算机视觉的lane detection 技术,在直道和匝道上实时检测车辆与车道线的位置关系并计算出曲线半径及偏离中心的距离。通过棋盘格方法实现了相机校正(包括畸变纠正和平面检测),并结合透视变换将道路图像转换为鸟瞰图以便后续分析;采用多通道颜色空间处理(HLS通道+梯度阈值)提取 lane lines;通过滑动窗口法对pixel坐标进行二次多项式拟合并计算出curvature 和 vehicle position relative to lane center;最终将检测结果还原到原图像并标注曲线半径及偏离中心距离信息(如图所示)。

完成项目后撰写了一份技术总结, 既作为个人复习资料存档, 也希望能帮助他人更好地理解和学习。

打算创建一个专注于无人驾驶领域的微信交流群, 欢迎感兴趣的朋友加入我的微信: wxl609278502, 群聊中请注明: 姓名-单位/学校

高速公路直道检测效果
高速公路匝道弯道检测效果

测试地点:南京

软硬件要求

  1. 通用型车载记录仪(允许轻微畸变;建议对存在较大畸变的设备执行图像校正)
  2. 配备 GPU 的工控机更加理想化;即使无 GPU 也同样适用
  3. 运行在 Ubuntu 16.04 操作系统上,并搭配 OpenCV 3.2 库使用

实现步骤

  1. 图像校正(针对相机存在明显的几何畸变情况)
  2. 截取目标区域(仅对包含车道线信息的目标区域执行处理)
  3. 采用几何变换方法(将感兴趣区域图片转换为俯视图坐标系)
  4. 根据不同的光照条件、清晰度和颜色特性设置相应的梯度阈值和颜色阈值,并对每种情况进行单独处理。
  5. 解析二进制图像以确定属于车道线的像素点位置
  6. 统计左右两侧峰值点作为起始坐标并作为初始拟合基准
  7. 通过二次曲线拟合算法建立模型,并对噪声点进行滤波或采用RANSAC算法去除异常数据以提高拟合精度
  8. 计算出车道曲线半径参数并评估车辆相对于车道中心的位置偏移量
  9. 展示可行域边界并结合曲率数值输出车辆偏移状态信息

此处将利用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

以下为其中一张棋盘格图片校正前后对比:

distorted

校正图片测试

复制代码
    #获取棋盘格图片
    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))
hls

在路面较浅且车道线呈黄色的区域中,在此场景下 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)
x_thred

获取左、右车道线的像素坐标
在对二进制图像进行处理时,由于存在一定数量的噪声像素(即干扰或异常数据),为了确保准确识别出 lane 边缘的位置和形状(即 lane boundary detection),需要首先识别出哪些 pixel 属于 lane 线区域。

首先要确定车道线的起始位置(位于图片底部中心位置)。因为 lane markings 区域在 x 轴上呈现明显的聚集范围,在此区域内进行分析会更加高效可靠。
由此可知, 将 images 分为左右两部分, 可以明显地观察到 lane markings 在 x 轴上的变化趋势。
需要注意的是, 左右两边区域在 x 轴上的 pixels 值变化明显地指向了 lane markings 的起始位置。

以下为测试片x轴的像素分布图:

histogram

车道像素点拟合
确定左右车道线的基准点后,通过二次曲线模型对车道边缘进行拟合计算。具体实施时,采用宽度为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)所得结果如下:

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

全部评论 (0)

还没有任何评论哟~