1. 基于车道线检测的路径规划基础

任务目标

本案例实现一个最小可运行的视觉车道线检测与路径点生成流水线:输入单张道路图像,输出左右车道线的拟合多项式系数,并基于车道中心生成一组可供下游控制模块使用的路径点序列(车辆坐标系下)。

环境与依赖

需要 Python 3.8+,并安装以下库:

pip install opencv-python numpy matplotlib

实现思路与关键步骤

算法按以下流水线处理单帧图像,不涉及视频序列或时序平滑:

  1. 透视变换:将前视摄像头图像转换为鸟瞰图,使车道线近似平行,便于多项式拟合。
  2. 二值化:结合颜色空间(HLS的S通道)与梯度阈值(Sobelx),提取车道线像素。
  3. 滑窗搜索:从图像底部直方图峰值确定左右车道起始点,用滑动窗口跟踪车道像素。
  4. 多项式拟合:对左右车道像素分别做二阶多项式拟合,得到 f(y) = a·y² + b·y + c。
  5. 路径点生成:在鸟瞰图坐标系下,按固定纵向间隔取左右车道中点,再经逆透视变换映射回车辆坐标系,输出路径点序列。

完整代码

import cv2
import numpy as np

class LaneDetector:
    def __init__(self):
        # 透视变换矩阵(假设输入图像尺寸 720x1280)
        src = np.float32([[580, 460], [700, 460], [1050, 720], [230, 720]])
        dst = np.float32([[320, 0], [960, 0], [960, 720], [320, 720]])
        self.M = cv2.getPerspectiveTransform(src, dst)
        self.Minv = cv2.getPerspectiveTransform(dst, src)
        self.nwindows = 9
        self.margin = 100
        self.minpix = 50
        self.y_eval = 720  # 车辆坐标系原点在图像底边中心

    def _binary(self, img):
        """生成二值图:S通道阈值 + Sobelx梯度阈值"""
        hls = cv2.cvtColor(img, cv2.COLOR_BGR2HLS)
        s_channel = hls[:, :, 2]
        s_binary = np.zeros_like(s_channel)
        s_binary[(s_channel >= 170) & (s_channel <= 255)] = 1

        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=5)
        abs_sobelx = np.absolute(sobelx)
        scaled_sobel = np.uint8(255 * abs_sobelx / np.max(abs_sobelx))
        sx_binary = np.zeros_like(scaled_sobel)
        sx_binary[(scaled_sobel >= 50) & (scaled_sobel <= 255)] = 1

        combined = np.zeros_like(sx_binary)
        combined[(s_binary == 1) | (sx_binary == 1)] = 1
        return combined

    def _find_lane_pixels(self, binary_warped):
        """滑窗搜索左右车道像素坐标"""
        histogram = np.sum(binary_warped[binary_warped.shape[0]//2:, :], axis=0)
        midpoint = histogram.shape[0] // 2
        leftx_base = np.argmax(histogram[:midpoint])
        rightx_base = np.argmax(histogram[midpoint:]) + midpoint

        window_height = binary_warped.shape[0] // self.nwindows
        nonzero = binary_warped.nonzero()
        nonzeroy = np.array(nonzero[0])
        nonzerox = np.array(nonzero[1])

        left_lane_inds = []
        right_lane_inds = []
        leftx_current = leftx_base
        rightx_current = rightx_base

        for window in range(self.nwindows):
            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 - self.margin
            win_xleft_high = leftx_current + self.margin
            win_xright_low = rightx_current - self.margin
            win_xright_high = rightx_current + self.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) > self.minpix:
                leftx_current = int(np.mean(nonzerox[good_left_inds]))
            if len(good_right_inds) > self.minpix:
                rightx_current = 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]
        return leftx, lefty, rightx, righty

    def detect(self, img):
        """主检测函数:返回左右多项式系数和车辆坐标系路径点"""
        binary = self._binary(img)
        warped = cv2.warpPerspective(binary, self.M, (img.shape[1], img.shape[0]))

        leftx, lefty, rightx, righty = self._find_lane_pixels(warped)
        if len(leftx) < 10 or len(rightx) < 10:
            return None, None, None

        left_fit = np.polyfit(lefty, leftx, 2)
        right_fit = np.polyfit(righty, rightx, 2)

        # 生成路径点(鸟瞰图坐标)
        ploty = np.linspace(0, warped.shape[0]-1, 20).astype(int)
        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]
        center_x = (left_fitx + right_fitx) / 2.0

        # 逆透视到车辆坐标系(x=图像列-中心,y=图像底边-行)
        pts = np.vstack([center_x, ploty]).T.astype(np.float32)
        vehicle_pts = cv2.perspectiveTransform(pts.reshape(-1, 1, 2), self.Minv).reshape(-1, 2)
        vehicle_pts[:, 0] -= img.shape[1] / 2.0
        vehicle_pts[:, 1] = img.shape[0] - vehicle_pts[:, 1]

        return left_fit, right_fit, vehicle_pts

# 使用示例
if __name__ == "__main__":
    detector = LaneDetector()
    img = cv2.imread("test_image.jpg")  # 输入道路图像
    if img is None:
        print("请提供 test_image.jpg")
    else:
        left_fit, right_fit, path_pts = detector.detect(img)
        if path_pts is not None:
            print("左车道系数:", left_fit)
            print("右车道系数:", right_fit)
            print("路径点序列 (x, y) 米/像素坐标系:")
            print(path_pts)
        else:
            print("未检测到足够车道线像素")

运行与验证

运行方式:将代码保存为 lane_detector.py,准备一张包含清晰车道线的道路图片命名为 test_image.jpg 放在同目录,执行:

python lane_detector.py

通过条件:程序正常输出左右车道各3个多项式系数,以及20个路径点坐标。路径点应呈现从近到远沿车道中心延伸的分布,且 x 坐标大致在图像中心附近(车辆坐标系原点),y 坐标从0(车头位置)递增到约720(远处)。若输出 None,说明图像中车道线对比度过低或弯道过急,可调整 _binary 中的S通道阈值(170)或Sobel梯度阈值(50)。

常见问题

  • 检测不到车道线:检查图像是否包含清晰的车道线;光照过强或过暗时,可降低S通道下限至150或提高Sobel下限至80。
  • 路径点抖动:本案例为单帧检测,未做时序平滑。实际应用中可对多项式系数做指数移动平均。
  • 透视矩阵不匹配:代码中的 src 点针对1280x720图像标定。若使用其他分辨率,需重新标定或先resize到1280x720。