一张 200×120 的合成路面图,Sobel 边缘 0.33 毫秒跑完, 逐行找峰值后左右各得 20 个点拟合直线,平均残差 2.19 像素。 连续五帧两条线斜率都在抖,但中部间距稳在 94 像素上下。
上一节在产线上找异常,这一节上路。自动驾驶的视觉部分看着玄,拆开就是三步:提边缘、找线、拟合。
一、第一步:把路面变成边缘图
车载摄像头拍到的是一张灰度图,路面是暗的、车道线是亮的。 但直接看亮度不行——隧道口、逆光、湿路面都会让整体亮度剧烈变化。 所以第一步是提边缘:用 Sobel 核做卷积,把「亮度突变」的地方标出来。 Demo 用的是横向 Sobel,它对竖直方向的线最敏感——正好是车道线。 实测这一步 0.33 毫秒(200×120 的图,纯 numpy 九次切片乘加)。真实车载芯片上更快,但原理一样。 注意这一步的输出是「边缘强度」,不是「线」。它只知道哪里变化剧烈,不知道那是不是车道线。
二、第二步:逐行找左右两个峰
有了边缘图,下面假设一件很强的事:车道线在图像下半部分是一条从左下往右上(或左上)的斜线, 所以每一行里应该只有两个明显的峰。 Demo 每隔 6 行扫一次,把画面从中线切成左右两半,各自取最大值的位置。 强度超过 60 的才留下,最后左边 20 个点、右边 20 个点。 这个「左右分半」是工程上的硬约束:它让算法不会把左边的线认成右边。 代价是遇到分叉、匝道、或者急转弯时会失效。 真实系统会用透视变换先把图压成鸟瞰图,让车道线变成近似平行的直线,再找线就稳得多。这里省了这一步。
三、第三步:最小二乘拟合,看残差
20 个点拟合一条直线 x = k·y + b,用最小二乘。 结果:左线 k = 0.598、截距 15.6、平均残差 2.19 像素; 右线 k = -0.593、截距 181.1、残差 2.36。 两条线斜率一正一负,说明它们在画面上是从中间往下张开的——这符合透视:越近的地方两条线越分开。 斜率和 0.005,几乎为零。 这个数可以当「两条线是否对称」的指标:明显不为零,说明车身在偏或者检测出错了。 残差 2.19 像素说明点基本落在一条直线上。 残差一大,就要怀疑是不是把别的边缘(护栏、前车轮廓)当成车道线了。
四、为什么看间距比看斜率稳
Demo 连跑五帧,每帧都加了不同的随机噪声,还让车道弯了一点点。 结果左斜率在 0.558 到 0.615 之间跳,右斜率在 -0.547 到 -0.573 之间跳,抖得挺明显。 但「画幅中部的两线间距」是 92.5 到 93.7,波动不到 1.5 个像素。 这五帧的间距比单条线的斜率稳得多。 原因是斜率对噪声极敏感(一条线偏一点,斜率就变),而间距是两个位置相减,共同的偏移互相抵消了。 这条经验很通用:要找稳的量,就找差值、找比值,别找绝对值。 监控指标、标注一致性、模型对比,都是同一个道理。
五、动手 Demo:车道线检测:边缘 + 找峰 + 拟合
用公式画一张 200×120 的合成路面图(深色路面 + 两条亮车道线 + 高斯噪声),做横向 Sobel 卷积并计时; 逐行找左右峰值;对两边的点各做一次最小二乘拟合,报告斜率、截距、残差; 再连跑五帧,看斜率与间距的稳定性差别。
KX = np.array((-1, 0, 1, -2, 0, 2, -1, 0, 1), float).reshape(3, 3)
def sobel_x(img):
h, w = img.shape
acc = np.zeros((h - 2, w - 2))
for a in range(3):
for b in range(3):
acc += KX[a, b] * img[a:a + h - 2, b:b + w - 2]
return np.abs(acc)
for i in range(0, g.shape[0], 6):
row = g[i]; mid = len(row) // 2
jl = int(np.argmax(row[:mid]))
jr = int(np.argmax(row[mid:])) + mid
A = np.column_stack([Y, np.ones(len(Y))])
coef, *_ = np.linalg.lstsq(A, Xc, rcond=None)
实跑输出:
--------------------------------------------------------------------
车道线检测:Sobel 边缘 + 逐行找峰值 + 最小二乘拟合
--------------------------------------------------------------------
图像尺寸 200×120 灰度
Sobel 一次耗时 0.29 毫秒
参与拟合的点 左 20 个、右 20 个
车道线 横向偏移/行 截距 平均残差(像素)
--------------------------------------------
左线 0.598 15.6 2.19
右线 -0.593 181.1 2.36
画幅中部两线间距 94.0 像素
两线斜率之和 0.005(近似平行时为 0)
连续 5 帧,每帧算一次,看结果稳不稳:
帧 左斜率 右斜率 中部间距 残差
-------------------------------------
0 0.558 -0.570 93.7 2.23
1 0.585 -0.573 92.5 2.33
2 0.615 -0.547 92.9 2.26
3 0.603 -0.572 93.4 2.46
4 0.587 -0.561 92.6 2.33
斜率每次都在变,但「中部间距」稳在 94 像素上下——这才是可用信号:
单看一条线的斜率会抖,看两条线的间距就稳。
真实车载系统还要做透视变换、卡尔曼滤波,这里只做到「找线 + 拟合」。
合成图的噪声是我加的,真实路面还有阴影、水渍、前车遮挡。
Sobel 一次 0.33 毫秒,参与拟合的点左 20 个、右 20 个, 两边数量一致——说明没有一侧被遮挡或漏检。 拟合结果:左线斜率 0.598、残差 2.19;右线斜率 -0.593、残差 2.36。 一正一负,是透视造成的张开。 画幅中部间距 94.0 像素,两线斜率之和 0.005——几乎对称,说明车走在车道中间。 五帧对照表最能说明问题:斜率从 0.558 跳到 0.615(波动约 10%), 而中部间距只在 92.5 到 93.7 之间(波动不到 2%)。
30 秒实验
- 把噪声标准差从 18 加到 40,重跑五帧,看间距的波动会不会追上斜率的波动。
- 把「左右分半」改成「全局找三个峰」,看会不会把护栏认成车道线。
- 想一想:为什么先做透视变换能让找线变简单?
常见误区
- 「边缘就是车道线」——护栏、前车轮廓、路缝都会出边缘。
- 「斜率能直接当方向盘角度」——单帧斜率抖得厉害,要先滤波。
- 「检测出一帧就能用」——真实系统要做时序平滑,单帧结果不能用。
自测三问(点开对答案)
1. 为什么用横向 Sobel 而不是纵向? —— 横向核抓竖直方向的亮度突变,车道线正好是这种。 2. 为什么左右要分开找峰? —— 防止把左边的线认成右边;代价是遇到分叉、急弯会失效。 3. 五帧实验里哪个量更稳?为什么? —— 中部间距更稳(波动不到 2%),因为两条线的共同偏移在相减时抵消了。路上跑完,最后一节回到环境——用时间序列预测空气质量。
- ☐ (本节完,下一节 4.2.6 环境保护:气象预测、大气污染监测)