🧠 第四章 机器学习算法
📄 一句话概括

一张 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 秒实验

  1. 把噪声标准差从 18 加到 40,重跑五帧,看间距的波动会不会追上斜率的波动。
  2. 把「左右分半」改成「全局找三个峰」,看会不会把护栏认成车道线。
  3. 想一想:为什么先做透视变换能让找线变简单?

常见误区

  1. 「边缘就是车道线」——护栏、前车轮廓、路缝都会出边缘。
  2. 「斜率能直接当方向盘角度」——单帧斜率抖得厉害,要先滤波。
  3. 「检测出一帧就能用」——真实系统要做时序平滑,单帧结果不能用。
自测三问(点开对答案) 1. 为什么用横向 Sobel 而不是纵向? —— 横向核抓竖直方向的亮度突变,车道线正好是这种。 2. 为什么左右要分开找峰? —— 防止把左边的线认成右边;代价是遇到分叉、急弯会失效。 3. 五帧实验里哪个量更稳?为什么? —— 中部间距更稳(波动不到 2%),因为两条线的共同偏移在相减时抵消了。

路上跑完,最后一节回到环境——用时间序列预测空气质量。

相关笔记