3. 车道线检测基础:图像预处理、霍夫变换与车道线拟合、逆透视变换

各位同学,大家好。今天我们进入车道保持功能的核心环节——车道线检测。说实话,这是整个系统的“眼睛”,眼睛不好使,后面控制再牛也白搭。我当年刚入行时,就因为在图像预处理上偷了个懒,结果实车测试时车道线识别一塌糊涂,被项目总监叫去“喝茶”……从那以后,我对这步就格外上心。

车道线检测,说白了就是让摄像头看懂路。但摄像头拍到的原始图像,噪声多、光照乱、透视变形严重。所以我们需要一套组合拳:先做图像预处理,再提取线条,最后把图像“拉直”成鸟瞰图。今天我们就一步步拆解这个过程。

3.1 图像预处理:灰度化、滤波、边缘检测

原始图像是彩色的,但颜色信息对车道线检测来说,其实有点“冗余”。你想想看,车道线无非是白色或黄色,我们真正关心的是亮度变化和边缘。所以第一步,把彩色图转成灰度图。

灰度化公式(加权平均法):

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

这个权重是模拟人眼对颜色的敏感度。绿色权重最高,因为人眼对绿光最敏感。

灰度化之后,图像里还有很多噪声——路面纹理、阴影、轮胎印。这些都会干扰后续的边缘检测。怎么办?滤波。

我个人习惯用高斯滤波。它像一个“模糊刷”,把每个像素和周围像素做加权平均,噪声就被抹平了。但注意,滤波核不能太大,否则车道线边缘也会被模糊掉。我在项目中一般用 5x5 或 7x7 的核,效果比较平衡。

import cv2
import numpy as np

# 读取图像
img = cv2.imread('road.jpg')
# 灰度化
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 高斯滤波
blurred = cv2.GaussianBlur(gray, (5, 5), 0)

滤波之后,就该边缘检测上场了。这里我们用的是 Canny 边缘检测。它有两个阈值参数:低阈值和高阈值。梯度强度高于高阈值的,肯定是边缘;低于低阈值的,直接丢弃;介于两者之间的,看是否与高阈值边缘相连。

避坑指南:我曾经在一条树影斑驳的路上测试,Canny 阈值设得太低,结果把树叶的影子也当成车道线了。后来我改成自适应阈值,或者根据图像亮度动态调整,效果就好多了。

Canny 边缘检测的典型参数:

edges = cv2.Canny(blurred, 50, 150)  # 低阈值50,高阈值150

嗯,这里要注意:阈值不是固定的。白天和夜晚、晴天和雨天,最佳阈值都不一样。我建议在代码里留一个可调参数,方便标定。

3.2 霍夫变换与车道线拟合

边缘检测之后,我们得到了一堆“可能是车道线”的像素点。但这些点还是散的,我们需要把它们拟合成直线。霍夫变换就是干这个的。

霍夫变换的原理,说白了就是“投票”。每个边缘点都可以对应无数条直线,但只有真正穿过车道线的那些直线,才会得到大量点的“投票”。我们设定一个阈值,得票高的直线就是我们要找的车道线。

在 OpenCV 中,我们常用的是概率霍夫变换(HoughLinesP)。它比标准霍夫变换更快,而且能直接给出线段的端点坐标。

lines = cv2.HoughLinesP(edges, rho=1, theta=np.pi/180, threshold=50,
                        minLineLength=100, maxLineGap=50)

参数说明:

参数 含义 典型值
rho 距离分辨率(像素) 1
theta 角度分辨率(弧度) π/180
threshold 投票阈值 50
minLineLength 最小线段长度 100
maxLineGap 同一条线上点的最大间隔 50

霍夫变换检测出来的线段,往往有很多条。我们需要把它们分类成左车道线和右车道线。怎么分?看斜率。

  • 左车道线:斜率为负(从左上到右下)
  • 右车道线:斜率为正(从左下到右上)

然后,对每一类的线段做最小二乘法拟合,得到一条完整的直线。我习惯用 np.polyfit 来做拟合:

# 假设 left_points 是左车道线上所有点的坐标
left_fit = np.polyfit(left_points[:, 1], left_points[:, 0], 1)
# left_fit[0] 是斜率,left_fit[1] 是截距

拟合出来的直线,我们一般用两个点来表示:图像底部的点和图像中间高度的点。这样方便后续的逆透视变换。

注意:如果某一边检测到的线段太少,或者拟合出来的直线斜率异常(比如接近水平),我建议直接丢弃这一帧的结果,用上一帧的数据做平滑。否则,车道线会跳来跳去,控制模块会疯掉的。

3.3 逆透视变换(IPM)

好了,现在我们在图像上找到了车道线。但问题是,摄像头是斜着看路的,远处的车道线在图像里会变窄、变模糊。这种透视效果,对后续的车道偏离计算非常不利。

逆透视变换(IPM),就是把这个透视图像“拉直”成鸟瞰图。从上方垂直往下看,车道线就是平行的,距离关系也变成线性的了。

IPM 的核心是找一个透视变换矩阵。我们需要在原始图像上选四个点,然后指定它们在鸟瞰图中的位置。这四个点通常选一个矩形区域,比如车辆前方 10 米到 50 米的范围。

# 原始图像上的四个点(梯形)
src_points = np.float32([
    [200, 720],   # 左下
    [600, 720],   # 右下
    [450, 400],   # 右上
    [150, 400]    # 左上
])

# 鸟瞰图上的四个点(矩形)
dst_points = np.float32([
    [200, 720],
    [600, 720],
    [600, 0],
    [200, 0]
])

# 计算透视变换矩阵
M = cv2.getPerspectiveTransform(src_points, dst_points)
# 应用变换
warped = cv2.warpPerspective(img, M, (img.shape[1], img.shape[0]))

变换之后,车道线在鸟瞰图中就是两条近乎平行的直线了。这时候再做车道偏离判断,就简单多了——看车辆中心到左右车道线的距离就行。

个人经验:IPM 的精度非常依赖标定。我曾经在一条有坡度的路上测试,因为没考虑路面坡度,鸟瞰图里的车道线歪得离谱。后来我加了一个俯仰角补偿,才解决问题。所以,如果你的车在上下坡时车道线检测不准,先检查 IPM 参数。

另外,IPM 之后的图像,远处会被拉伸得很厉害,分辨率下降。所以一般我们只关注车辆前方 30 米以内的区域,再远就不太可靠了。

小结

今天的内容,我们走完了车道线检测的完整流程:

  1. 灰度化:去掉颜色冗余,保留亮度信息
  2. 滤波:高斯滤波去噪声,保护边缘
  3. 边缘检测:Canny 算法提取车道线边缘
  4. 霍夫变换:从边缘点中投票选出直线
  5. 车道线拟合:分类、拟合、平滑
  6. 逆透视变换:把图像拉成鸟瞰图,方便后续计算

每一步都有坑,但每一步也都有成熟的解决方案。下一节课,我们会基于这些检测到的车道线,来做车道偏离预警和车道保持控制。到时候你会发现,前面这些预处理工作做得越扎实,后面的控制就越轻松。

好,今天就到这里。有问题随时交流。