用Python和OpenCV搞定车道线曲率计算:从图像处理到实际距离转换的保姆级教程
用Python和OpenCV实现车道线曲率与车辆位置检测全流程解析车道线检测是自动驾驶和高级驾驶辅助系统ADAS中的基础功能之一。准确计算车道线的曲率半径和车辆相对于车道中心的位置对于保持车辆在车道内安全行驶至关重要。本文将带你从零开始使用Python和OpenCV构建一个完整的车道线检测系统重点解决从像素坐标到实际物理距离转换这一关键问题。1. 环境准备与相机标定任何基于视觉的车道线检测系统第一步都是确保相机成像的准确性。由于镜头畸变会导致图像失真我们必须先进行相机标定。1.1 安装必要依赖在开始前请确保已安装以下Python库pip install opencv-python numpy matplotlib1.2 相机标定实战相机标定的核心是找到相机的内参矩阵和畸变系数。我们使用棋盘格图像来完成这一过程import cv2 import numpy as np import glob # 设置棋盘格内角点数量 nx 9 # 每行内角点数量 ny 6 # 每列内角点数量 # 准备对象点(0,0,0), (1,0,0), (2,0,0) ..., (8,5,0) objp np.zeros((nx*ny,3), np.float32) objp[:,:2] np.mgrid[0:nx,0:ny].T.reshape(-1,2) # 存储对象点和图像点的数组 objpoints [] # 真实3D点 imgpoints [] # 图像2D点 # 读取所有标定图像 images glob.glob(camera_cal/calibration*.jpg) for fname in images: img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 查找棋盘格角点 ret, corners cv2.findChessboardCorners(gray, (nx,ny), None) if ret True: objpoints.append(objp) imgpoints.append(corners) # 可选绘制并显示角点 cv2.drawChessboardCorners(img, (nx,ny), corners, ret) cv2.imshow(img, img) cv2.waitKey(500) cv2.destroyAllWindows() # 相机标定 ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)提示实际应用中建议使用15-20张不同角度的棋盘格图像进行标定以获得更准确的结果。2. 图像预处理与车道线提取获得相机参数后我们需要对原始图像进行去畸变处理然后提取车道线特征。2.1 图像去畸变def undistort_image(img, mtx, dist): 去除图像畸变 return cv2.undistort(img, mtx, dist, None, mtx) # 测试去畸变效果 test_img cv2.imread(test_images/test1.jpg) undistorted undistort_image(test_img, mtx, dist)2.2 车道线特征提取车道线提取通常结合颜色空间转换和边缘检测def lane_detection_pipeline(img, s_thresh(170, 255), sx_thresh(40, 200)): 车道线检测流水线 img np.copy(img) # 转换到HLS颜色空间 hls cv2.cvtColor(img, cv2.COLOR_RGB2HLS).astype(np.float32) l_channel hls[:,:,1] s_channel hls[:,:,2] # Sobel边缘检测 sobelx cv2.Sobel(l_channel, cv2.CV_64F, 1, 0) abs_sobelx np.absolute(sobelx) scaled_sobel np.uint8(255*abs_sobelx/np.max(abs_sobelx)) # 基于梯度的二值化 sxbinary np.zeros_like(scaled_sobel) sxbinary[(scaled_sobel sx_thresh[0]) (scaled_sobel sx_thresh[1])] 1 # 基于颜色的二值化 s_binary np.zeros_like(s_channel) s_binary[(s_channel s_thresh[0]) (s_channel s_thresh[1])] 1 # 结合两种二值化结果 combined_binary np.zeros_like(sxbinary) combined_binary[(s_binary 1) | (sxbinary 1)] 1 return combined_binary # 应用车道线检测 binary_image lane_detection_pipeline(undistorted)3. 透视变换与车道线拟合为了计算曲率我们需要将图像转换为鸟瞰视角这样可以更直观地观察车道的弯曲程度。3.1 透视变换实现def perspective_transform(img, src_pointsNone): 执行透视变换 if src_points is None: # 默认源点根据实际图像调整 src np.float32([ [580, 460], # 左上 [700, 460], # 右上 [1040, 680], # 右下 [260, 680] # 左下 ]) else: src np.float32(src_points) img_size (img.shape[1], img.shape[0]) offset 300 # 偏移量 # 目标点 dst np.float32([ [offset, 0], # 左上 [img_size[0]-offset, 0], # 右上 [img_size[0]-offset, img_size[1]], # 右下 [offset, img_size[1]] # 左下 ]) # 计算变换矩阵 M cv2.getPerspectiveTransform(src, dst) Minv cv2.getPerspectiveTransform(dst, src) # 执行变换 warped cv2.warpPerspective(img, M, img_size, flagscv2.INTER_LINEAR) return warped, M, Minv # 应用透视变换 warped_image, M, Minv perspective_transform(binary_image)3.2 车道线多项式拟合在鸟瞰图中我们可以使用二次多项式拟合车道线def fit_lane_lines(binary_warped): 拟合车道线 # 计算直方图确定车道线大致位置 histogram np.sum(binary_warped[binary_warped.shape[0]//2:,:], axis0) midpoint np.int(histogram.shape[0]//2) leftx_base np.argmax(histogram[:midpoint]) rightx_base np.argmax(histogram[midpoint:]) midpoint # 滑动窗口参数 nwindows 9 window_height np.int(binary_warped.shape[0]//nwindows) margin 100 minpix 50 # 识别非零像素 nonzero binary_warped.nonzero() nonzeroy np.array(nonzero[0]) nonzerox np.array(nonzero[1]) # 当前窗口位置 leftx_current leftx_base rightx_current rightx_base # 存储左右车道线像素索引 left_lane_inds [] right_lane_inds [] # 遍历窗口 for window in range(nwindows): # 窗口边界 win_y_low binary_warped.shape[0] - (window1)*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 # 识别窗口内的非零像素 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] left_lane_inds.append(good_left_inds) right_lane_inds.append(good_right_inds) # 如果找到足够像素调整下一个窗口位置 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])) # 合并索引 left_lane_inds np.concatenate(left_lane_inds) right_lane_inds np.concatenate(right_lane_inds) # 提取左右车道线像素位置 leftx nonzerox[left_lane_inds] lefty nonzeroy[left_lane_inds] rightx nonzerox[right_lane_inds] righty nonzeroy[right_lane_inds] # 二次多项式拟合 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, nonzerox, nonzeroy # 拟合车道线 left_fit, right_fit, left_lane_inds, right_lane_inds, nonzerox, nonzeroy fit_lane_lines(warped_image)4. 曲率计算与车辆位置检测4.1 从像素到实际距离的转换这是整个系统的关键部分。我们需要定义像素与实际距离的转换关系# 定义转换参数 ym_per_pix 30/720 # y方向每像素对应的米数 (基于图像高度720像素≈30米) xm_per_pix 3.7/700 # x方向每像素对应的米数 (标准车道宽度3.7米≈700像素)4.2 曲率半径计算基于多项式拟合结果我们可以计算车道线的曲率半径def calculate_curvature(yvals, left_fit, right_fit, ym_per_pix, xm_per_pix): 计算车道线曲率半径 # 选择计算曲率的位置图像底部 y_eval np.max(yvals) # 将多项式转换为实际世界坐标 left_fit_cr np.polyfit(yvals*ym_per_pix, left_fit*xm_per_pix, 2) right_fit_cr np.polyfit(yvals*ym_per_pix, right_fit*xm_per_pix, 2) # 计算曲率半径 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]) return left_curverad, right_curverad # 生成y值 ploty np.linspace(0, warped_image.shape[0]-1, warped_image.shape[0]) left_curverad, right_curverad calculate_curvature(ploty, left_fit, right_fit, ym_per_pix, xm_per_pix)4.3 车辆位置检测计算车辆相对于车道中心的位置def calculate_vehicle_position(img, left_fit, right_fit, xm_per_pix): 计算车辆相对于车道中心的位置 # 图像底部 y_max img.shape[0] # 计算左右车道线在图像底部的位置 left_x left_fit[0]*y_max**2 left_fit[1]*y_max left_fit[2] right_x right_fit[0]*y_max**2 right_fit[1]*y_max right_fit[2] # 计算车道中心位置 lane_center (left_x right_x)/2 # 计算车辆中心位置假设相机安装在车辆中心 vehicle_center img.shape[1]/2 # 计算偏移距离像素 offset_pixels vehicle_center - lane_center # 转换为实际距离 offset_meters offset_pixels * xm_per_pix return offset_meters # 计算车辆位置 vehicle_offset calculate_vehicle_position(warped_image, left_fit, right_fit, xm_per_pix)5. 结果可视化与优化建议5.1 可视化检测结果def draw_lane_info(original_img, warped_img, left_fit, right_fit, Minv, curvature, offset): 在原始图像上绘制车道线和信息 # 创建空白图像绘制车道线 warp_zero np.zeros_like(warped_img).astype(np.uint8) color_warp np.dstack((warp_zero, warp_zero, warp_zero)) ploty np.linspace(0, original_img.shape[0]-1, original_img.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] # 重新整理点以绘制填充多边形 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)) # 在鸟瞰图上绘制车道 cv2.fillPoly(color_warp, np.int_([pts]), (0, 255, 0)) # 将鸟瞰图转换回原始视角 newwarp cv2.warpPerspective(color_warp, Minv, (original_img.shape[1], original_img.shape[0])) # 合并结果 result cv2.addWeighted(original_img, 1, newwarp, 0.3, 0) # 添加曲率信息 avg_curvature (curvature[0] curvature[1]) / 2 cv2.putText(result, Radius of Curvature: {:.2f}m.format(avg_curvature), (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2) # 添加车辆位置信息 if offset 0: position_text Vehicle is {:.2f}m right of center.format(abs(offset)) elif offset 0: position_text Vehicle is {:.2f}m left of center.format(abs(offset)) else: position_text Vehicle is at center cv2.putText(result, position_text, (50, 100), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2) return result # 可视化结果 result_image draw_lane_info(undistorted, warped_image, left_fit, right_fit, Minv, (left_curverad, right_curverad), vehicle_offset)5.2 实际应用中的优化建议动态调整转换参数不同相机和安装位置会影响ym_per_pix和xm_per_pix的值建议在实际应用中通过实地测量校准这些参数。滤波与平滑处理在视频处理中可以对连续帧的检测结果进行加权平均避免曲率估计的剧烈变化。异常检测机制当左右车道线的曲率差异过大时可能是检测错误应触发重新检测或使用历史数据。多方法融合结合深度学习语义分割方法可以提高在复杂场景下的检测鲁棒性。硬件加速对于实时性要求高的应用可以考虑使用OpenCV的CUDA模块或移植到C实现。这套系统在实际测试中表现良好但在极端光照条件或车道线模糊的情况下可能需要额外的处理逻辑。建议在实际部署前进行充分的道路测试根据具体场景调整参数和算法。