让车跑起来(一):不训练模型的那条路
这一章讲三件事: 从这里开始有三条并列的路,而这本书三条全走了、还全都装到了实车上; 第一条路一个模型都不训,靠颜色把车道线挑出来; 以及这条路真正的代价 —— 换个光照就得重调一遍,参数是人拿滑块试出来的。 它在全书链条里的位置是第 11 级台阶的第一段:前十章都在铺路,从这一章起才是「让车真的跑」。 这一章的主走查是一张 640×480 的车前道路图,从进来一路走到两个前轮的转速差。 遇到的生词都在当场解释。
1. 顶层全景
同一个任务:让小车沿着车道线自己跑一圈
┌─ 路线一(本章) 一个模型都不训。车道线周围铺了绿色,
│ 那就按颜色把它挑出来 → 算偏差 → 转成角速度
│
├─ 路线二(13 章) 训一个卷积网,吃一张图,直接吐出那个 −1 到 1 的数
│
└─ 路线三(14 章) 训一个卷积网,给图上每一个 像素判一个类别,
再从中间抠出中心线
⇒ **三条路在这本书里全部走完了,而且都真的装到了实车上。**
这是它在整个书架上不可替代的地方。
图说:三条路的输入完全一样(一张车前道路图),输出的形状完全不同。
三条路怎么选,第 14 章有一张我们做的对比表。
本章这一条路的全程:
一张 640×480 的车前道路图
│
│ ① 转成灰度图 —— 第 04 章讲过
▼
问题来了:图里除了车道线,还有标识物、障碍物、背景 (第 3 节)
│
│ ② 挖一个梯形的**蒙版**,把不关心的地方挡掉 (第 4 节)
▼
剩下的那一块叫**感兴趣区域**
│
│ ③ 在 HSV 上定一组**阈值**,把车道线挑出来 (第 5 节)
▼
一张只有 0 和 255 的二值图,里面只剩车道线
│
│ ④ 由它算出「车偏了多少」,乘上一个比例 (第 7、8 节)
▼
一个角速度 →(折成 −1 到 1 的转角)→ 两个前轮的转速差 → 车拐了
↑ 这层换算书没给 ↑ 第 02 章讲过
图说:这条链上**没有任何一个数是学出来的**。
② 的梯形顶点、③ 的六个阈值、④ 的那个比例,全部是人坐在那儿一点点调的。
2. 为什么这么朴素的办法居然够用
先说这条路的前提:赛场是特意布置过的
这是全章最要紧的一句话,漏了它,后面每一步都会显得像巧合:
2023 年那一届的地图场景,是用绿色把车道线与图像背景分隔开的1。
换句话说:赛场自己帮了大忙。 车道线周围铺了一层和它颜色差别很大的绿, 于是「把跟周围颜色不一样的那几条挑出来」就够用了。
书还给了这条路成立的第二个条件:比赛规则允许—— 规则允许用传统图像处理的方法、训练深度学习模型的方法、或者图像分割技术 来完成车道巡航任务1。三条路都是合规的,不是走捷径。
这场比赛在模拟什么
书把这件事放到了一个更大的坐标里2:
自动驾驶车辆常使用纯视觉方案来检测车道线,例如特斯拉和 Mobileye 的纯视觉自动驾驶方案。 这场比赛用摄像头感知周围环境,可以将其理解为对现实世界纯视觉感知方案的模拟。
「纯视觉方案」就地解释: 车上只装摄像头,不装激光雷达、不装毫米波雷达, 所有关于「周围有什么、路在哪儿」的判断都从图片里得出来。 它的对立面是「多传感器融合」 —— 摄像头 + 雷达一起用,互相印证。 (这两条路线各自的取舍,书没有展开,我们也不在这里展开。)
书还提了一句这条路上别的办法: 除了本节要讲的颜色阈值分割, 还有边缘检测、大津法、霍夫变换、最小二乘法拟合等等3。 书选了最简单的那一种,并且明说「本节只介绍最基本的原理」4。
3. 第一步之后就卡住:灰度图里不止车道线
这是本章的承重点,而且它是这条路上第一个真问题。
现象先行:转成灰度,然后呢
第 04 章讲过怎么把彩色图转成灰度图 —— 三个通道压成一个 0—255 的亮度值。
书拍了一张车前的实景图,转完灰度之后指出: 除了车道线之外,场景中还有很多别的对象 —— 车道两旁的标识物、障碍物、地图场景的背景元素5。
这些东西在灰度图上是什么样?和车道线一样,就是一堆 0—255 的数。 灰度值不认识「这是车道线、那是路牌」。
拿具体的数看这个问题有多硬
(下面这组像素值是我们为演示编的 —— 书的图片本体不在清洗文本里。)
同一张图上取三个点,先看它们的彩色值(按第 04 章讲的 OpenCV 顺序:蓝、绿、红):
A 黄色车道线上一点 (B= 40, G=200, R=230)
B 旁边的绿色地面 (B= 70, G=180, R= 60)
C 路边一块白色标识物 (B=238, G=240, R=242)
转成灰度(用通用的加权公式 0.299×红 + 0.587×绿 + 0.114×蓝):
A → 0.299×230 + 0.587×200 + 0.114×40 = 68.8 + 117.4 + 4.6 = **191**
B → 0.299× 60 + 0.587×180 + 0.114×70 = 17.9 + 105.7 + 8.0 = **132**
C → 0.299×242 + 0.587×240 + 0.114×238 = 72.4 + 140.9 + 27.1 = **240**
现在试着定一条灰度阈值,想只留下车道线:
阈值取 160 ⇒ A(191)过,B(132)不过 ✓ 但 C(240)也过了 ✗
阈值取 200 ⇒ C(240)过,而 A(191)自己反倒不过 ✗✗
⇒ **无论阈值定在哪儿,那块白色标识物都比车道线还亮,挡不掉。**
图说:这就是书说的「有必要找到一种方法来忽略图像中的无关物体」。
**注意问题不在阈值定得不好,在于灰度这个维度上,白牌子和黄线本来就分不开。**
两条出路,书两条都用了:
| 出路 | 做什么 | 在哪一节 |
|---|---|---|
| 在空间上排除 | 白牌子在画面上方,车道线在下方 —— 把上方整块挡掉 | 第 4 节(蒙版) |
| 在颜色上排除 | 白牌子几乎没有颜色,黄线颜色很纯 —— 换一套坐标来判 | 第 5 节(HSV 阈值) |
4. 蒙版:把不关心的地方直接挡掉
书给的定义,朴素得让人放心
蒙版是一个 numpy 数组。 当需要对图像中的一部分进行遮盖时, 只需将图像中对应区域的像素值改为 0 或 255,或任何数字5。
第 04 章埋的那个账在这里还了:「蒙版就是一个 numpy 数组」—— 不是什么新东西,就是第 04 章那种「一堆同类型的数,可以整块一起算」。
蒙版长这样(和原图一样大,640×480):
关心的位置 → 写 255
不关心的位置 → 写 0
用法也朴素:**两张图对应位置相乘,再除以 255**
原图那个点 191 × 255 ÷ 255 = 191 (留住了)
原图那个点 240 × 0 ÷ 255 = 0 (抹掉了)
图说:所以蒙版不是什么图形学操作,就是一次逐像素的乘法。
(乘完除以 255 这个写法是我们补的;书只说了「把对应区域的像素值改为 0 或 255」。)
蒙版为什么挖成梯形
书给的理由是一句物理事实:
由于摄像头角度的影响,车道线所在区域通常为近大远小的梯形区域5。
摄像头架在车头,斜着往前看 ——
画面上方 = 远处 车道在这里挤成窄窄一条
画面下方 = 近处 车道在这里铺得很宽
┌──────────────────────┐ y=0 (远)
│ │
│ ╱────╲ │ y=260 ← 梯形的上底,窄
│ ╱ ╲ │
│ ╱ ╲ │
│ ╱ ╲ │
└────╱────────────╲────┘ y=480 ← 梯形的下底,宽(近)
0 640
我们给它四个顶点(这组坐标是为演示编的,书没给具体数):
(0, 480) (640, 480) (420, 260) (220, 260)
图说:**梯形不是为了好看,是为了配合透视。**
挖成矩形的话,上半部分会把远处的天空、路牌、观众全框进来;
挖成梯形,那些东西自动在框外。
套上蒙版之后剩下的那一块,书叫感兴趣区域(常写作 ROI,Region of Interest)6。 这个缩写你出门会撞见 —— 任何一份图像处理的文档、任何一个检测模型的说明里都有它。
上一节那个白色标识物,到这里已经被挡掉了 —— 它在画面上方 y=180 的位置,在梯形之外。
5. 阈值:为什么调的是 HSV,不是 RGB
现象先行:光一变,数就全变 了
假设你在灰度上或者在 RGB 上定阈值。 赛场的灯亮一点,同一条黄线的三个通道 会一起往上抬 —— 红绿蓝三个数全变了,你定的三条线全部失效。
书给的做法:在 HSV 上调,而且是拖着滑块试出来的
书没有讲原理,它讲的是现场怎么操作 —— 而这恰恰是这条路的真面目7:
① 启动校准程序,屏幕上出现**两个窗口**:
左边:摄像头原图
右边:处理后的二值图(只有黑和白)
② 再开一个终端,启动**参数调节器** —— 一排可以拖的滑块
③ **拖 HSV 那几个数,一边拖一边看右边那张二值图**,
直到车道线干干净净地留下、别的全变黑
④ 把调好的那组数字**手抄下来**,填进 follow.py 里对应的位置
图说:注意第 ③ 步的判据是「看着顺眼」。
**没有损失函数,没有梯度,没有训练 —— 判据是人的眼睛。**
这就是「不训练模型」这条路的字面意思。
为什么是 HSV(第 04 章埋的账在这里还)
第 04 章讲过三套色彩坐标各为谁而设: RGB 是为硬件设计的(屏幕上三种灯管各出多少力), HSV 是 1978 年为视觉感知设计的 —— 三个数分别是:
| 字母 | 中文 | 管什么 |
|---|---|---|
| H | 色调 | 是什么颜色 —— 红、黄、绿、蓝,排成一个圈 |
| S | 饱和度 | 颜色有多纯 —— 0 是灰的,越大越鲜艳 |
| V | 亮度 | 有多亮 |
关键就在于它把「是什么颜色」和「有多亮」拆成了两个数。
拿第 3 节那三个点换算成 HSV(这组数是我们为演示编的):
A 黄色车道线 H= 28 S=200 V=230
B 绿色地面 H= 60 S=150 V=180
C 白色标识物 H= 0 S= 10 V=240
定一组阈值:H 在 20—35 之间、S ≥ 100、V ≥ 120
A:H=28 ✓ S=200 ✓ V=230 ✓ → **过,留下**
B:H=60 ✗ → 挡掉(色调不对,它是绿的)
C: S= 10 ✗ → 挡掉(**几乎没有颜色,是灰白的**)
⇒ 三个点里只剩车道线。**而在灰度上,这三个点怎么都分不开。**
现在把赛场的灯调暗一点看看:
A 变成 H= 28 S=195 V=160 ← **H 几乎没变,只有 V 掉了**
同一组阈值:H=28 ✓ S=195 ✓ V=160 ✓ → **还是过**
图说:这就是 HSV 的全部价值 ——
**光照变化主要压在 V 上,而「是什么颜色」由 H 管,基本不受影响。**
你在 RGB 上定阈值,三个数会一起塌;在 HSV 上定,塌的 主要是一个。
⚠ 但要老实说:这只是「不那么容易塌」,不是「不会塌」。 光照变化大到一定程度,S 和 H 也会漂。这就引出第 6 节那条代价。
6. 这条路的代价
先看书给的四条对策
书承认这套办法在图像上方检测不够准确,并给了四条对策8:
| 对策 | 具体做什么 | 代价 |
|---|---|---|
| 缩小感兴趣区域 | 梯形挖小一点,只看更靠近车身的那一段 | 看得更近,弯道来不及提前反应 |
| 调整阈值 | 把那六个数再拖一遍 | 人工,而且只对这个光照有效 |
| 使用滤波函数 | 先做一次模糊,把零星的噪点抹掉 | 细节也一起被抹掉 |
| 使用自适应阈值 | 不用全图一个阈值,按每一小块各自的明暗定各自的阈值 | 计算量上去了,而且参数更多 |
(第三、四条那两句「代价」是我们补的;书只列了对策名,没有讲每条的取舍。)
真正的代价书没有明说,但它就摆在流程里
判断(我们的,不是书里的):这条路真正的成本,是那组阈值不是模型的一部分, 而是「现场那一天那个光照下的一组常数」。
依据就是第 5 节那套操作流程本身:阈值是人拖着滑块调出来、手抄进代码里的。 所以只要下面任何一件事发生,就得回到第 5 节从头拖一遍: 换个场馆、换个时段(窗外的自然光变了)、开了另一组灯、 换一块新印的地图布(印刷色差)、甚至只是有人站在旁边挡了一片影子。
这不是这条路做得不好,这是这条路的定义。 它把「适应环境」这件事整个外包给了人。
如果错,会错在: 如果场地和光照完全固定(比如封闭实验室里跑一个固定演示), 那这条代价根本不会发生 —— 那种场合下这条路是最优解,又快又不用数据。 判据是:这个场地的光照条件,一年里会不会变。
这条判断在第 13、14 章会各被对照一次: 那两条路把成本换到了别处 —— 一条压在「得有人真的开车跑出几千张带转角的图」上, 一条压在「得有人一个像素一个像素地描边」上。第 14 章有一张三条路的完整对比表。