第3章:车道线检测基础

各位同学,欢迎来到第三章。上一章我们聊了摄像头标定,算是把「眼睛」调准了。这一章,我们要让这双眼睛真正「看懂」车道线。

车道线检测,说白了就是在一张图像里找到那些白色的、黄色的线条。听起来简单?我刚开始做的时候,被各种光照、阴影、路面纹理折磨得够呛。嗯,咱们一步步来。

3.1 图像预处理:把图像变「干净」

原始图像信息量太大,直接处理效率低,还容易受干扰。所以我们要做预处理。我个人习惯分三步走:灰度化、高斯滤波、边缘检测。

3.4.1 灰度化

彩色图像有RGB三个通道,每个像素点包含的信息量是灰度图的3倍。但车道线检测,颜色信息其实没那么重要——我们关心的是亮度变化和边缘。

灰度化公式很简单:

Gray = 0.299 * R + 0.587 * G + 0.114 * B

为什么权重不一样?因为人眼对绿色最敏感,对蓝色最不敏感。这个公式是ITU-R BT.601标准,我建议你直接用OpenCV的 cv2.cvtColor(),它内部已经实现了。

我的经验: 有些同学喜欢自己写灰度化函数,其实没必要。OpenCV的底层优化做得很好,比自己手写快得多。我在项目里踩过这个坑——手写版本在嵌入式平台上跑不动。

3.4.2 高斯滤波

图像里总有噪声。摄像头传感器噪声、路面颗粒感、甚至空气中的灰尘,都会在图像上形成「椒盐」。高斯滤波就是用来平滑这些噪声的。

它的原理很简单:用一个高斯核(比如5x5的矩阵)在图像上滑动,每个像素的新值等于它周围像素的加权平均。权重由高斯分布决定——离中心越近,权重越大。

# 我常用的参数
kernel_size = 5  # 核大小,必须是奇数
sigma = 1.0      # 标准差
blurred = cv2.GaussianBlur(image, (kernel_size, kernel_size), sigma)
注意: 核大小不是越大越好。核太大,边缘信息会被过度平滑,车道线边缘就模糊了。我曾经在高速场景下用过9x9的核,结果车道线边缘完全糊掉,霍夫变换根本检测不到线。后来改成5x5,效果立竿见影。

3.4.3 边缘检测(Canny算法)

滤波之后,我们要找边缘。Canny算法是业界标准,我用了快十年,没换过别的。

Canny的核心步骤:

  1. 计算梯度:用Sobel算子算x方向和y方向的梯度,得到每个像素的梯度幅值和方向。
  2. 非极大值抑制:只保留梯度方向上的局部最大值,其他像素置0。这一步让边缘变细。
  3. 双阈值检测:设定高阈值和低阈值。高于高阈值的肯定是边缘,低于低阈值的肯定不是。介于两者之间的,如果与强边缘相连,则保留。
# 我常用的Canny参数
low_threshold = 50
high_threshold = 150
edges = cv2.Canny(blurred, low_threshold, high_threshold)

为什么阈值要设成50和150?其实没有固定值。我一般先设成1:3的比例,然后根据实际场景微调。你想想看,如果阈值设得太低,路面纹理都会被当成边缘;设得太高,车道线可能断断续续。

避坑指南: 我曾经在一个隧道场景里翻车了——隧道内光照极暗,Canny阈值按白天设的,结果车道线完全检测不到。后来我加了自适应阈值,根据图像平均亮度动态调整,才解决问题。

3.2 霍夫变换与车道线拟合

拿到边缘图之后,我们要从这些离散的边缘点里「提取」出直线。霍夫变换就是干这个的。

3.2.1 霍夫变换原理

霍夫变换的核心思想:把图像空间中的点,映射到参数空间中的曲线。在图像空间中,一条直线由斜率和截距决定(y = kx + b)。但在参数空间中,我们用极坐标表示:

ρ = x * cos(θ) + y * sin(θ)

其中ρ是原点到直线的距离,θ是直线的法线角度。为什么用极坐标?因为斜截式在垂直线时会遇到无穷大斜率,不好处理。

具体做法:

  1. 对于边缘图上的每个点,遍历所有可能的θ(通常0到180度),计算对应的ρ。
  2. 在参数空间(ρ, θ)中投票,每个点投一票。
  3. 找到投票数超过阈值的(ρ, θ)组合,这些就是检测到的直线。
# OpenCV中的霍夫变换
lines = cv2.HoughLinesP(
    edges, 
    rho=1,           # ρ的精度,1像素
    theta=np.pi/180, # θ的精度,1度
    threshold=50,    # 投票阈值,越大检测到的直线越少
    minLineLength=40, # 最小线段长度
    maxLineGap=10     # 同一条线上最大允许间隔
)
我的习惯: 我一般先用 HoughLinesP(概率霍夫变换),它比标准霍夫变换快很多,而且直接返回线段端点。在嵌入式平台上,这个速度差异非常明显。

3.2.2 车道线拟合

霍夫变换检测到的是一堆线段,但我们要的是完整的车道线。所以需要做拟合。

我常用的方法:

  1. 分类:根据斜率把线段分成左车道线和右车道线。左车道线斜率为正(图像坐标系),右车道线斜率为负。
  2. 剔除离群点:用RANSAC或者简单的统计方法,去掉明显偏离的线段。
  3. 最小二乘拟合:对剩下的点做直线拟合,得到最终的左右车道线方程。
# 伪代码示意
left_lines = [line for line in lines if slope(line) > 0]
right_lines = [line for line in lines if slope(line) < 0]

# 拟合左车道线
left_points = np.concatenate([line.reshape(-1, 2) for line in left_lines])
left_fit = np.polyfit(left_points[:, 0], left_points[:, 1], 1)
注意: 拟合时一定要考虑图像坐标系的原点位置。OpenCV中,原点在左上角,y轴向下。所以左车道线的斜率是正的,右车道线的斜率是负的。我见过有同学搞反了,结果车道线画到了对面车道。

3.3 逆透视变换(IPM)原理

前面我们都是在图像坐标系下处理车道线。但图像是透视投影,远处的车道线看起来更窄,近处的更宽。这给后续的车道保持控制带来了麻烦——你没法直接用图像坐标计算车辆偏离距离。

逆透视变换(IPM)就是解决这个问题的。它把透视图像转换成鸟瞰图,让车道线变成平行的。

3.3.1 透视变换与逆透视变换

透视变换描述的是三维世界到二维图像的映射。逆透视变换就是反过来,从图像恢复出俯视图。

数学上,透视变换可以用一个3x3的单应矩阵表示:

[x']   [h11  h12  h13] [u]
[y'] = [h21  h22  h23] [v]
[w']   [h31  h32  h33] [1]

其中,x = x'/w', y = y'/w'

要计算这个矩阵,我们需要知道4个对应点。在车道线检测中,通常选择图像中的四个点(比如车道线的四个角点),映射到鸟瞰图中的矩形。

# 我常用的IPM实现
src_points = np.float32([
    [200, 720],   # 左下
    [600, 720],   # 右下
    [550, 450],   # 右上
    [250, 450]    # 左上
])

dst_points = np.float32([
    [300, 720],
    [980, 720],
    [980, 0],
    [300, 0]
])

M = cv2.getPerspectiveTransform(src_points, dst_points)
warped = cv2.warpPerspective(image, M, (1280, 720))
关键点: 源点的选择非常关键。选得太宽,会引入太多路面干扰;选得太窄,车道线可能被截断。我一般根据摄像头安装位置和视野范围来标定,然后在实车上微调。

3.3.2 IPM的局限性

IPM不是万能的。它假设路面是平面,但实际道路有坡度、起伏。遇到上坡时,远处的车道线会被拉长;下坡时会被压缩。

我记得有一次在高速上测试,遇到一个长上坡,IPM出来的鸟瞰图完全变形了,车道线变成了曲线。后来我加了坡度补偿,根据车辆俯仰角动态调整IPM参数,才解决这个问题。

另外,IPM对摄像头标定精度很敏感。如果外参(尤其是俯仰角和偏航角)有偏差,鸟瞰图会歪斜。所以,我建议你在做IPM之前,先确保摄像头标定是准确的。

3.4 本章小结

这一章我们走完了车道线检测的完整流程:

  • 灰度化:减少计算量,保留亮度信息
  • 高斯滤波:去除噪声,平滑图像
  • Canny边缘检测:提取边缘特征
  • 霍夫变换:从边缘点中检测直线
  • 车道线拟合:得到完整的车道线方程
  • 逆透视变换:转换成鸟瞰图,便于后续控制

下一章,我们会把这些技术整合起来,实现一个完整的车道线检测模块。到时候,我会分享一些我在实车测试中遇到的坑和解决方案。咱们下章见。