1. 三个感知任务为何要放进同一个系统
先从一个实际场景说起。我最早接触这个方向,是在做一个面向园区低速自动驾驶的感知Demo。当时的需求很简单:车辆行驶到路口时,既要知道当前红灯还是绿灯,也要知道前车在哪里、和我的距离有多近,同时还得在停止线前稳稳停住。这三个问题看起来是相互独立的——检测灯、分割车、找线,但真正跑起来才发现,它们必须放在同一个感知框架里联动处理,否则任何一个环节出错,整个决策链路都会出问题。
红绿灯识别提供的是纵向控制的时间依据,告诉我"现在能不能走";停止线识别提供的是空间位置依据,告诉我"该在哪里停";车辆分割提供的是对周围交通参与者的精细理解,告诉我"附近有没有车、车占多大空间"。三者合在一起,才构成一个路口场景下完整的感知闭环。如果只做红绿灯识别,车停到了停止线中间甚至越过停止线,依然有安全隐患;如果只做车辆分割,遇到路口没有前车可跟的时候,红绿灯状态就成了唯一的控制信号。
这篇文章就围绕"红绿灯识别、车辆分割、停止线识别"这三个任务,把我实际搭建这套系统时用到的模型选型、数据集构建、训练技巧、部署优化和踩坑经验完整梳理一遍。整体用到的技术栈是深度学习目标检测、实例分割、语义分割,加上一些经典的图像处理算法做几何约束。适合正在做辅助驾驶感知、机器人导航,或者想入门自动驾驶视觉方案的同学参考。
2. 红绿灯识别:小目标检测的模型选型与数据工程
2.1 为什么红绿灯检测比想象中要难
红绿灯识别属于典型的小目标检测问题。一个标准的灯头在高分辨率图像里可能只占几十乘几十个像素,加上灯盘本身结构复杂——有灯壳、发光区域、背板,不同方向还有箭头灯、圆盘灯、倒计时数字。早期我试过直接把通用目标检测模型拿来做,效果惨不忍睹,不是漏检就是误检。后来才总结出几个关键难点:
第一是观察距离远。车辆距离路口五六十米时,灯头区域在1080P图像里通常不超过40×40像素,许多检测模型的下采样倍数过大,浅层特征根本没有足够的细节保留。第二是发光特性导致过曝。红灯亮起时,高亮区域周围的像素会溢出,灯头轮廓变得不规则,模型容易把整个黄色灯壳当成红色区域。第三是类别不均衡。真实路采数据里,绿灯的样本量远大于红灯和黄灯,因为通行时间本来就长;加上夜间数据、逆光数据、雨雾数据的覆盖不足,模型很容易在绿灯上过拟合。
2.2 模型选型:精度与延迟的权衡
我在这个任务上先后尝试了三类方案,这里给出一份基于实测的对比表格:
| 方案 | 输入分辨率 | 单帧延迟(车载GPU) | 小目标AP | 适用场景 |
|---|---|---|---|---|
| YOLOv5s + 全图检测 | 640×640 | 约6ms | 红灯0.71 | 快速验证、中期测试 |
| YOLOv8s + 区域裁剪放大 | 1280×1280 | 约12ms | 红灯0.84 | 正式项目主方案 |
| Faster R-CNN + 特征金字塔 | 1000×600 | 约38ms | 红灯0.88 | 离线测试、数据集挖掘 |
如果不考虑部署算力限制,Faster R-CNN系列在两阶段检测器里对小目标的召回率确实更高,因为区域提议网络天然会去覆盖不同尺度的候选框。但在实际车载设备上,我最终选择的是YOLOv8s配合"先检测后裁剪放大"的策略:先用一个轻量模型检测灯头大致的区域,然后把该区域从原始图中切出来,送入另一个模型或同一模型的高分辨率输入分支做精细分类。这个思路和人类看灯的习惯很像——先找到灯在哪,再看它到底是什么颜色。
2.3 数据集构建:自采数据与自动挖掘
数据集是红绿灯识别项目里最花时间的部分。公开数据集当然可以用来做预训练,但真实路口的灯型五花八门:竖向排列的、横向排列的、带倒计时的、不带倒计时的、合金灯壳和塑料灯壳的,甚至还有被树枝遮挡一半的。我自己的做法是三路并进:
- 车载相机录制的连续视频,按关键帧抽帧,保存原始大图。
- 用初版检测模型跑一遍所有抽帧图像,把置信度在0.3到0.7之间的"模糊样本"挑出来,这些是挖掘新难例的重点对象。
- 对挑选出的难例做人工标注,标注框只覆盖发光区域,不包含整个灯壳。这样做的好处是让模型直接学习颜色区域的边界,而不是学习灯壳的背景纹理。
类别定义上,我没有把红灯、绿灯、黄灯分成三个类别之后就结束,而是额外增加了"不亮/未知"类别。因为真实场景里经常出现灯灭掉、闪烁过渡、或者被强光反射导致颜色无法判断的情况。强制模型在所有状态里输出红绿黄,会导致决策系统在灯熄灭时做出错误判断。加入"不亮"这个类别后,状态机可以明确进入"未知"分支,宁可等待也不贸然通过,安全逻辑清晰很多。
2.4 数据增强与小目标trick
针对小目标检测,我试过几种增强策略,最有效的是"拼接增强"和"局部放大增强"的组合。拼接增强把多张图拼接成大图,迫使模型在不同尺度下学习灯的上下文;局部放大增强则把标注区域随机裁剪放大后粘贴到新背景中,相当于人为制造了大量近景灯头样本,缓解了近距离样本不足的问题。
训练时还有几个细节值得记录:
- 使用多尺度训练,输入尺寸在960到1600之间随机切换,比固定大分辨率更稳。
- 损失函数中增加针对小目标的权重系数,让模型对面积小于32×32像素的检测框更加敏感。
- 推理阶段对灯头区域做轻度锐化处理,有利于边缘特征提取,但过度锐化会引入噪声,需要控制在卷积核半径3以内。
3. 车辆分割:从矩形框到像素级轮廓的价值跃迁
3.1 为什么纯检测框不够用
车辆检测输出的是外接矩形框,但矩形框在路口场景里有三个天然缺陷。第一,车辆转向时,框内会混入大量地面背景,导致后续距离估计出现偏差;第二,前后车辆重叠时,矩形框互相遮挡,单纯的非极大值抑制很难区分到底是一辆车还是两辆车;第三,框本身不带朝向信息,无法判断前车是否正在变道。正因为这些限制,我在完成了第一阶段的红绿灯识别后,立刻把注意力转向了车辆分割,让系统输出车辆轮廓的精细掩码。
这里用的是实例分割,它在语义分割的基础上额外区分同一个类别下的不同个体。直观理解就是:语义分割告诉我图像里哪些像素属于"车",实例分割告诉我这些像素分属于"第一辆车""第二辆车"等不同对象。路口的车辆往往成排出现,实例分割的必要性非常高。
3.2 分割方案实测对比
我分别测试过Mask R-CNN、YOLACT和SparseInst三条路线。Mask R-CNN的掩码质量最高,但推理速度太慢,在嵌入式设备上只能跑到5帧左右,难以满足实时控制要求。YOLACT的速度优势明显,但掩码边缘比较粗糙,对于轮廓精细度要求不高的场景已经完全够用。SparseInst的参数量较小,不过在近距离大目标上的效果不如YOLACT稳定。
工程上我采用的折中方案是"检测引导分割":先用目标检测模型输出车辆框,把每个框裁剪出来,再送入一个轻量级语义分割网络,只对框内区域做前景背景分割,最后通过连通域分析把同一辆车被分割成多块的情况合并。这样做的好处是大幅缩小了分割网络的输入范围,模型不需要在整张大图上学习全局上下文,精度和速度反而都提升了。
3.3 掩码后处理与距离估算
分割模型输出的原始掩码需要经过一系列后处理才能用于决策。首先是形态学开运算,消除掩码边缘的孤立噪点;然后是轮廓提取,用轮廓的最大内接矩形替代原始的检测框,得到更贴合车身的包络;最后是轮廓面积占比计算——如果掩码面积和检测框面积的比值低于某个阈值,说明检测框内大部分是背景,这个框极有可能是误检,可以直接过滤掉。
掩码的真正价值体现在距离估计上。我之前用单目相机做距离估计时,以检测框底边中点作为地面接触点,这个方法在车尾正对相机时精度尚可,但一旦车辆斜停,框底边中点对应的实际地面点会偏移。有了分割掩码后,可以计算掩码底缘轮廓的中点和宽度,配合相机标定参数,使用针孔模型反投影到地面坐标系,解出车辆到相机的距离。实测下来,在30米范围内,这个方案的测距误差从原来的15%降到了8%左右。
3.4 分割模型的训练注意点
分割任务对标注质量极为敏感,尤其是车辆边缘。标注时如果对轮胎和车身的边界处理得含糊,模型收敛后会输出"抹泥"一样的模糊边界。我自己的标注规范是车轮与地面接触线必须精确到像素,车窗反光区域按车身处理,拖车和挂斗连接处单独标成两个实例。这样一个二类实例分割任务,标注一张复杂路口的图大约需要8分钟,整个训练集积累了近3万张图后才达到稳定效果。
训练时还有一个容易踩的坑:类别不平衡不仅发生在类别之间,还发生在尺度之间。远距离的小车像素占比小,在损失函数中天然处于劣势,模型倾向于把远处的多个小车合并成一个糊块。解决方法是把远距离样本单独做一个尺度分组,在训练时对包含大量小目标的图像提高采样权重,同时减少大目标样本的重复率。
4. 停止线识别:被低估的定位瓶颈
4.1 停止线的物理特征与检测难点
停止线在路口地面是一条横向白色实线,宽度通常为20到40厘米,长度覆盖对应车道的宽度。从图像特征上看,它和车道线、人行横道线、导向箭头都属于地面标线,很容易混淆。难点在于停止线本身没有颜色特异的发光体,完全依赖纹理和几何上下文来判断。
最初我尝试直接用语义分割模型把停止线当作一个类别输出。效果很快出了问题:模型在晴天的固定路口表现尚可,但换个路口或者路面有新铺沥青的深色背景,分割结果就变得支离破碎。原因在于分割模型学到的更多是"白色横向线条"这个视觉模式,而不是"车道末端靠近路口的横向白实线"这个语义概念。
4.2 视觉分割 + 几何约束的联合方案
最终可行的方案是语义分割和几何约束的结合。流程拆开来看:
第一步,用轻量级语义分割网络在车道区域内检测所有白色标线像素。这个网络不区分停止线和车道线,只回答"这里是不是白色标线"。第二步,对分割得到的标线掩码做逆透视变换,把前视图像投影到鸟瞰视角。第三步,在鸟瞰图中对掩码像素做直线拟合,找出所有长度较长、接近水平的候选线。第四步,利用车道级先验——停止线必然位于车道末端,且其前方通常有斑马线或路口区域——从候选中筛选出真正的停止线。
这里最关键的是逆透视变换参数。相机安装位置和俯仰角固定后,通过标定板求取单应性矩阵,把图像平面映射到地面平面。这样做的好处是把远处因透视而压缩的线条重新拉直,拟合直线时不容易被近处的弧线干扰。实测在平直路面上,停止线的横向位置定位误差可以控制在0.3米以内。
4.3 如何与红绿灯识别形成联动
停止线识别不是孤立存在的,它与红绿灯识别之间有一层逻辑配合关系。通俗地说:红绿灯告诉你何时停,停止线告诉你在哪停。两者如果缺乏约束,会出现"绿灯亮了但车越过停止线"或"车在停止线前停住了,但因为灯的状态识别错误又贸然启动"的尴尬局面。
我实现的状态判断逻辑很简单:只有同时满足"信号灯状态为可通行"且"车辆当前位置在停止线以内"两个条件时,才允许系统给出起步指令。如果车辆已经越过停止线,即使检测到绿灯,也需要保持低速观察,由后续的车辆分割结果来辅助判断路口内是否有冲突目标。反过来,如果车辆尚未到达停止线且信号灯为红灯,系统会计算当前车速到停止线的距离,决定是平稳减速还是紧急制动。这套联动逻辑在模拟环境里跑了大几百个路口场景,误判率比单独使用任一模块时明显下降。
4.4 恶劣天气下的兜底策略
雨雪天气里,地面标线被积水覆盖或被积雪遮挡,视觉方案几乎失效。这里我用了一个不算复杂但很可靠的兜底策略:高精度地图先验。在车辆驶入路口前两三百米处,地图上已经存有停止线的精确经纬度坐标。视觉识别到停止线后,与地图坐标做匹配,如果一致性超过阈值,则信任视觉结果;如果视觉结果完全缺失,则直接采用地图坐标,结合当前车辆的定位数据计算停止线距离。这样即使视觉彻底失效,系统依然能够完成基本的停车任务,只是精度略有下降。
5. 训练与部署阶段的踩坑清单
5.1 训练阶段的坑:标注不一致与损失震荡
第一个大坑是标注不一致。多个标注员对同一张图的灯头区域标注范围理解不同,有人把发光区加灯壳都框进去了,有人只框发光区,导致模型训练时一会儿学这个模式一会儿学那个模式,损失函数在低谷附近反复震荡。解决办法是项目启动前用30张典型图像统一做标注评审,把争议样本拉出来逐一定标准,并生成一份带示例图的标注规范文档。
第二个大坑是验证集和训练集划分不当。红绿灯数据是按视频连续帧抽的,相邻帧之间的场景几乎相同。如果随机划分,训练集里出现的场景会大量泄露到验证集,导致验证精度虚高。后来我改成按视频片段划分,同一个路口的连续帧只会出现在同一个集合里,验证结果才真正反映了模型对陌生路口的泛化能力。
第三个坑是损失函数的数值震荡。实例分割训练早期,掩码分支的损失经常出现尖刺,后来排查发现是锚点采样策略中正负样本比例失衡所致。把正负样本比例限制在1比3以内,并给掩码损失增加梯度裁剪后,训练曲线平稳了很多。
5.2 部署阶段的坑:推理速度与精度不可兼得
部署时最直观的矛盾是模型精简和精度的此消彼长。我把红绿灯检测模型从FP16量化到INT8之后,单帧延迟下降了40%,但红灯类别的AP掉了接近5个百分点。排查发现,灯头像素的高频边缘信息在INT8量化时被过度压缩,模型丢失了颜色边界的分辨能力。我的处理方式是分割量化敏感层——对检测头部分的卷积层保留FP16,只对主干网络使用INT8量化,最终在延迟只增加2毫秒的情况下,把精度损失控制在了1.5%以内。
车载推理框架我用的是TensorRT加速,配合多线程流水线:相机帧采集线程、预处理线程、推理线程、后处理线程并行,让单帧端到端延迟稳定在35毫秒左右。这里特别提醒一点:不要忽略图像缩放和归一化在CPU上消耗的时间。一开始我把整张1080P图像缩放和归一化放在预处理线程里顺序执行,结果这部分占到总延迟的25%。后来改用GPU上的缩放算子,配合固定的输入尺寸和预计算均值方差,延迟明显下降。
5.3 容易忽略的测试场景
测试阶段如果只挑晴天顺光的路口跑,很多问题暴露不出来。我后来建立了一套专门的压力测试场景表:
| 场景类型 | 常见问题 | 我的处理方式 |
|---|---|---|
| 逆光直射 | 红绿灯高光溢出、轮廓消失 | 对图像做局部自适应均衡后再送检 |
| 夜间霓虹灯干扰 | 红灯误检率升高 | 加入夜间样本训练,利用灯盘背景色做先验 |
| 灯闪烁过渡 | 红绿跳变 | 状态机要求连续3帧确认才切换 |
| 路面反光积水 | 停止线分割断裂 | 融合红外补光通道建模 |
| 前方大型车辆遮挡 | 信号灯完全不可见 | 切换为跟车策略 |
这里想重点说下"灯闪烁过渡"。部分路口倒计时结束时灯具切换瞬间会出现几十毫秒的暗场,普通模型在暗场里会推断出"灭灯"状态,紧接着下一帧又是红色,状态在极短时间内跳变两次。对下游控制模块来说,这种跳变会引起刹车踏板踩放抖动。用状态机做时序滤波,只有连续3帧以上检测到同一状态才更新系统状态,能有效滤除单帧噪声。
5.4 端到端联调中的逻辑冲突
模块单独跑都在水准以上,但合到一起部署后,我发现了一个很隐蔽的逻辑冲突:车辆分割模块在检测到前方有大量排队车辆时,会给控制模块一个"前方拥堵"的信号,控制模块因此降低了巡航速度;但红绿灯检测模块输出的绿灯状态又要求尽快通过路口。两个决策打架,导致车辆在绿灯状态下缓慢蠕动,后方车辆不断鸣笛催促。
排查后确认,问题出现在多模块输出没有统一到一个决策置信度体系里。后来我在感知融合层给不同模块增加了置信度加权机制:红绿灯状态在连续3帧一致时置信度最高,车辆分割输出的拥堵判断必须结合前车平均速度来做,只有当平均速度低于某阈值时,拥堵信号的权重才会上升。这样两个模块形成互补而不是互斥,绿灯起步行为和拥堵缓行行为被正确分离。
6. 这套方案的可改进方向与个人复盘
整个项目做下来,最大的体会是:视觉感知模型的精度固然重要,但把三个任务当成一个整体去设计交互逻辑,才是系统真正稳定运行的关键。红绿灯识别给出时间语义,停止线识别给出空间边界,车辆分割给出周围目标的形状和位置,三者之间通过状态机、置信度加权和先验地图约束连成闭环,缺一环都跑不出"路口安全通行"这个结果。
如果后续要继续深入,我认为值得尝试的方向有三个。一是引入时序建模,用轻量级的时序网络代替纯单帧检测,把信号灯的闪烁历史、前车的运动轨迹纳入判断,进一步提高状态识别的鲁棒性。二是加深语义地图的融合力度,不只在停止线识别时用地图先验,还可以把路口的车道拓扑关系做成先验信息,帮助红绿灯识别在遮挡条件下做推理。三是在车辆分割里加入目标跟踪,让系统不只认识"这里有一辆车",还知道"这辆车在过去几秒里如何运动",这对判断路口冲突风险至关重要。
最后分享一个实操中的小技巧:给所有模型保存不同训练阶段的checkpoint,不要只留最终版。遇到新场景掉精度时,回退到中间某个checkpoint做增量训练,往往比在最终模型上硬调效果更好,也更容易定位是哪个训练阶段引入的偏差。这套流程我反复用过很多次,省下的重训时间相当可观。
代码和数据都是死的,真正让系统跑得稳的,是排错时一点点积累出来的那份"场景直觉"。希望这篇把红绿灯识别、车辆分割和停止线识别串起来的复盘,能给正在做同样事情的你提供一些切实可用的参考。