3. 车道线检测算法基础:图像预处理与直线检测
各位同学,今天我们进入车道线检测的核心环节。说实话,这部分内容我做了不下上百次实验,踩过的坑比你们想象的多得多。但别担心,我会把那些血泪教训都揉碎了讲给你们听。
车道线检测,说白了就是让摄像头“看懂”路面上的白线或黄线。但摄像头拍到的原始图像,嗯,那叫一个乱——有阴影、有反光、有路面纹理,甚至还有前车的影子。所以,我们得先做图像预处理,把有用的信息提取出来。
3.1 图像灰度化:去掉颜色干扰
为什么要把彩色图变成灰度图?我个人习惯是:颜色信息在车道线检测中其实用处不大。你想想看,车道线无非是白色或黄色,但路面颜色千变万化,反而容易干扰算法。
灰度化的公式很简单:
Gray = 0.299 * R + 0.587 * G + 0.114 * B
这个公式模拟了人眼对亮度的感知。绿色权重最高,因为人眼对绿色最敏感。我在项目中遇到过一个问题:有些摄像头对红色通道特别敏感,导致灰度图偏亮。后来我调整了权重系数,才解决了这个问题。
3.2 高斯滤波:去除噪声
图像噪声是绕不开的坎。摄像头传感器本身就有热噪声,再加上路面颠簸、光照变化,噪声无处不在。高斯滤波就是用来平滑图像、去除噪声的。
它的原理很简单:用一个高斯核(比如3x3或5x5的矩阵)去卷积图像。每个像素的值被替换为它周围像素的加权平均值,权重由高斯分布决定。
// 5x5高斯核示例
1/273 * [
1, 4, 7, 4, 1,
4, 16, 26, 16, 4,
7, 26, 41, 26, 7,
4, 16, 26, 16, 4,
1, 4, 7, 4, 1
]
我曾经在一条乡间小路上测试算法,结果发现车道线边缘全被模糊掉了。后来才意识到,高斯核选太大了。对于车道线这种细长结构,3x3的核就够用了,5x5反而会丢失细节。
3.3 Canny边缘检测:找到边界
好了,现在图像干净了,接下来要找到车道线的边缘。Canny边缘检测是我最常用的方法,没有之一。它分四步走:
- 计算梯度:用Sobel算子算出每个像素的梯度强度和方向。
- 非极大值抑制:只保留梯度方向上的局部最大值,把其他像素置零。
- 双阈值检测:设定高阈值和低阈值,高于高阈值的保留,低于低阈值的丢弃,介于两者之间的看是否与高阈值像素相连。
- 边缘连接:把断开的边缘连起来。
这里有个坑,我踩过好几次:双阈值的设定。高阈值太高,车道线边缘会断断续续;低阈值太低,又会引入大量噪声。我的经验是:高阈值取150-200,低阈值取50-80,具体数值要根据实际场景微调。
// OpenCV中的Canny调用示例
cv::Mat edges;
cv::Canny(gray_img, edges, 150, 80, 3);
3.4 霍夫变换直线检测:从点到线
边缘检测之后,我们得到了一堆像素点。但这些点怎么变成直线呢?霍夫变换就是干这个的。
它的思想很巧妙:把图像空间中的点,映射到参数空间中的曲线。在参数空间中,如果多条曲线交于一点,就说明这些点在图像空间中位于同一条直线上。
常用的霍夫变换有两种:
| 类型 | 参数空间 | 适用场景 |
|---|---|---|
| 标准霍夫变换 | (ρ, θ) | 检测完整直线 |
| 概率霍夫变换 | (ρ, θ) + 线段端点 | 检测线段,速度快 |
我个人更推荐概率霍夫变换。为什么?因为它只随机采样一部分点,计算量小,而且直接输出线段端点,省去了后续的线段提取步骤。
// 概率霍夫变换示例
std::vector<cv::Vec4i> lines;
cv::HoughLinesP(edges, lines, 1, CV_PI/180, 50, 30, 10);
参数说明:
rho:距离分辨率,一般取1像素theta:角度分辨率,一般取1度(即π/180)threshold:累加器阈值,值越大,检测到的直线越少但越可靠minLineLength:最小线段长度,太短的线段可能是噪声maxLineGap:同一条直线上两点之间的最大间隔
3.5 车道线拟合:最小二乘法
霍夫变换检测到的直线,往往有多条。我们需要从中筛选出左右两条车道线,然后用最小二乘法拟合出它们的方程。
最小二乘法的原理很简单:找到一条直线,使得所有点到这条直线的垂直距离的平方和最小。数学上,就是求解一个线性方程组。
// 最小二乘法拟合直线
// 输入:点集 (x_i, y_i)
// 输出:斜率 k 和截距 b
double sum_x = 0, sum_y = 0, sum_xy = 0, sum_x2 = 0;
int n = points.size();
for (int i = 0; i < n; i++) {
sum_x += points[i].x;
sum_y += points[i].y;
sum_xy += points[i].x * points[i].y;
sum_x2 += points[i].x * points[i].x;
}
double k = (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x * sum_x);
double b = (sum_y - k * sum_x) / n;
这里有个细节:车道线在图像中通常是斜的,而且越靠近车头越宽。所以,我建议只取图像下半部分的点进行拟合,上半部分的点(比如远处的车道线)误差太大,反而会拉偏拟合结果。
3.6 完整流程总结
好了,我们把整个流程串起来:
- 读取摄像头图像
- 灰度化:去掉颜色信息
- 高斯滤波:去除噪声
- Canny边缘检测:提取边缘
- 霍夫变换:检测直线
- 筛选左右车道线:根据斜率和位置
- 最小二乘法拟合:得到车道线方程
这个流程看起来简单,但每个环节都有坑。我建议你们先在离线数据集上跑通,再移植到嵌入式平台上。嗯,说到嵌入式平台,下一章我会讲如何在资源受限的MCU上优化这些算法,敬请期待。