第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(),它内部已经实现了。
3.4.2 高斯滤波
图像里总有噪声。摄像头传感器噪声、路面颗粒感、甚至空气中的灰尘,都会在图像上形成「椒盐」。高斯滤波就是用来平滑这些噪声的。
它的原理很简单:用一个高斯核(比如5x5的矩阵)在图像上滑动,每个像素的新值等于它周围像素的加权平均。权重由高斯分布决定——离中心越近,权重越大。
# 我常用的参数
kernel_size = 5 # 核大小,必须是奇数
sigma = 1.0 # 标准差
blurred = cv2.GaussianBlur(image, (kernel_size, kernel_size), sigma)
3.4.3 边缘检测(Canny算法)
滤波之后,我们要找边缘。Canny算法是业界标准,我用了快十年,没换过别的。
Canny的核心步骤:
- 计算梯度:用Sobel算子算x方向和y方向的梯度,得到每个像素的梯度幅值和方向。
- 非极大值抑制:只保留梯度方向上的局部最大值,其他像素置0。这一步让边缘变细。
- 双阈值检测:设定高阈值和低阈值。高于高阈值的肯定是边缘,低于低阈值的肯定不是。介于两者之间的,如果与强边缘相连,则保留。
# 我常用的Canny参数
low_threshold = 50
high_threshold = 150
edges = cv2.Canny(blurred, low_threshold, high_threshold)
为什么阈值要设成50和150?其实没有固定值。我一般先设成1:3的比例,然后根据实际场景微调。你想想看,如果阈值设得太低,路面纹理都会被当成边缘;设得太高,车道线可能断断续续。
3.2 霍夫变换与车道线拟合
拿到边缘图之后,我们要从这些离散的边缘点里「提取」出直线。霍夫变换就是干这个的。
3.2.1 霍夫变换原理
霍夫变换的核心思想:把图像空间中的点,映射到参数空间中的曲线。在图像空间中,一条直线由斜率和截距决定(y = kx + b)。但在参数空间中,我们用极坐标表示:
ρ = x * cos(θ) + y * sin(θ)
其中ρ是原点到直线的距离,θ是直线的法线角度。为什么用极坐标?因为斜截式在垂直线时会遇到无穷大斜率,不好处理。
具体做法:
- 对于边缘图上的每个点,遍历所有可能的θ(通常0到180度),计算对应的ρ。
- 在参数空间(ρ, θ)中投票,每个点投一票。
- 找到投票数超过阈值的(ρ, θ)组合,这些就是检测到的直线。
# OpenCV中的霍夫变换
lines = cv2.HoughLinesP(
edges,
rho=1, # ρ的精度,1像素
theta=np.pi/180, # θ的精度,1度
threshold=50, # 投票阈值,越大检测到的直线越少
minLineLength=40, # 最小线段长度
maxLineGap=10 # 同一条线上最大允许间隔
)
HoughLinesP(概率霍夫变换),它比标准霍夫变换快很多,而且直接返回线段端点。在嵌入式平台上,这个速度差异非常明显。
3.2.2 车道线拟合
霍夫变换检测到的是一堆线段,但我们要的是完整的车道线。所以需要做拟合。
我常用的方法:
- 分类:根据斜率把线段分成左车道线和右车道线。左车道线斜率为正(图像坐标系),右车道线斜率为负。
- 剔除离群点:用RANSAC或者简单的统计方法,去掉明显偏离的线段。
- 最小二乘拟合:对剩下的点做直线拟合,得到最终的左右车道线方程。
# 伪代码示意
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)
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边缘检测:提取边缘特征
- 霍夫变换:从边缘点中检测直线
- 车道线拟合:得到完整的车道线方程
- 逆透视变换:转换成鸟瞰图,便于后续控制
下一章,我们会把这些技术整合起来,实现一个完整的车道线检测模块。到时候,我会分享一些我在实车测试中遇到的坑和解决方案。咱们下章见。