在我自己动手做自动驾驶场景图像分割之前,一直觉得“感知”就是“把目标框出来”,直到第一次在真实街景数据上一帧一帧看分割结果,才彻底改变了想法——框只能告诉你“那里有辆车”,但车辆具体占了哪几条车道、路沿和积水边界在哪里,框全给不了。后来我做了几个月基于Unet的语义分割实验,从数据集整理、标签体系设计,到训练损失调整、推理后处理和嵌入式部署,踩了不少坑,也攒了一堆真正有用的经验。这篇文章就把整个过程拆开讲,思路和细节都比原项目更完整,适合刚入门分割方向、或者准备把分割模型落到实车/边缘设备上的同学参考。
1. 为什么自动驾驶里"分割"比"检测"更贴近真实路况
自动驾驶感知模块里,目标检测长期是主角,因为它的输出直观:一张图,若干个矩形框,每个框带类别和置信度。但一旦到真实路况,检测的局限性就会逐渐暴露出来。
先说最典型的问题:类别重叠与目标遮挡。市区路口一辆公交车斜停在画面里,另一辆自行车刚好经过它前方,检测框往往会合二为一,或者把自行车的框整个吞进公交车框里。行人三五成群并肩走,每个人的框都巨大且互相重叠,框的数量可能大于实际人数。对于决策模块来说,这种框级输出非常含糊——到底哪个像素属于哪辆车?哪个像素是背景?决策层拿到框之后,往往还需要二次计算目标的外接轮廓、相对位置,而这些信息已经从像素级被压缩丢失了。
分割则完全不同。语义分割给每个像素分配一个类别标签,输出是一张和原图一样尺寸的“类别图”。车、路、行人、建筑、天空、植被,每个像素都有归属。这种逐像素的空间描述天然适合下游的可行区域计算、可通行空间估计、车道线拟合和障碍物测距。换句话说,检测回答的是“有什么”,分割回答的是“哪里占着、哪里能走”,后者更接近驾驶决策真正需要的信息。
再说一个现实中的例子:雨天路面反光严重,检测模型常常把低矮路沿错检成车道线,或者在湿滑路面上把倒影识别成物体。分割模型在这类场景下同样会犯错,但因为输出是区域级的,可以在后续做时空滤波,把一个像素点在连续若干帧里的类别稳定性统计出来,误检区域的形状通常是杂乱不连续的,而真实目标的形状在时序上变化平滑,这个滤波逻辑在检测框上几乎没法做。
所以,一般感知系统里,检测负责找目标、跟踪目标,分割负责恢复几何和空间关系,两者是互补关系。对于起步阶段的自动驾驶项目来说,分割模型往往比检测模型更能体现感知系统对场景的理解深度。Unet作为最常用的分割结构之一,实现成本低、迭代速度快,非常适合作为第一个切入方向。
2. Unet为什么能成为这个场景的主力模型
Unet最早是给医学图像分割设计的,结构上并不复杂——一个收缩路径提取特征,一个扩张路径恢复分辨率,中间用跳跃连接把浅层细节和深层语义拼在一起。但恰恰是这套设计,在自动驾驶街景分割里意外地合适。
2.1 编码器-解码器结构到底做了什么
编码器部分可以理解为连续的卷积和下采样,逐层把图像压缩成高维语义特征图。这个过程很像读文章:先看单个字,再看整句话,最后提炼段落主旨。到了最深层,特征图尺寸可能只有原图的1/32,但每个特征点的“视野范围”覆盖了原图中很大一块区域,所以它能判断“这里是一条很宽的道路”“那边是一整片建筑”。
解码器部分做的就是反操作,把粗粒度的语义信息逐步上采样回原分辨率。这里的关键不只是放大像素,而是要恢复类别边界的位置细节。如果只用双线性插值放大特征图,边界会很糊,因为最深层特征图丢失了边缘位置信息。
Unet给出的解决办法是跳跃连接:在解码器的每一层,把编码器同分辨率层输出的特征图直接拼过来。低层特征保存了大量纹理、边缘、颜色变化信息,正好补充解码过程中丢失的空间细节。在自动驾驶场景里,车道线边缘、行人轮廓、路沿这种高频信息恰恰决定了分割边界的质量,跳跃连接带来的收益非常明显。
2.2 和其他分割模型的适用边界
很多人问为什么不直接用DeepLab系列或者SegNet。我的回答是看项目阶段和平台条件。DeepLab系列用空洞卷积扩大感受野,边界效果很好,但模型体积和计算量明显更大,部署到低算力设备上比较吃力。SegNet用池化索引恢复位置,参数少,但分割边缘细节一般。Unet在结构上处于一个平衡点:实现简单,改造成本低,backbone可以灵活替换成MobileNet、ResNet甚至更轻量的结构,训练稳定性也不错,尤其适合小规模和中期项目。
当然,Unet也不是万能的。如果做的是超大规模城市场景分割,要求极细的类别区分,并且有充足算力,那基于Transformer的分割模型或者DeepLab系列可能上限更高。换模型之前先想一想:当前瓶颈到底在模型结构,还是在数据与标签质量?我在项目里经常发现,数据不平衡和标签错标造成的误差,远大于结构差异带来的收益。
3. 数据集准备:比模型更影响结果的环节
很多人拿到分割任务第一反应是调模型结构,但我做了几轮实验之后发现,数据准备才是第一位的。分割模型的每个像素都是标签,这意味着标注成本和噪声都会被放大。模型结构差不多的情况下,数据集里的标签错误率会直接变成精度上限。
3.1 类别体系怎么设计
自动驾驶分割的类别体系不能照搬通用数据集,需要和下游决策对齐。我在初版方案里设计了二十几个类别,结果训练时发现频次过于悬殊:路面和建筑占了绝大多数像素,而摩托车、交通标志、自行车这些关键类别占比极低。模型很容易把“数量多”和“重要”混为一谈。
建议按这个思路收敛类别:
- 第一优先级:可行驶区域(路面)、车道线、车辆、行人、骑行者的像素级类别。这几个类别直接参与路径规划和安全判断,必须单独分出来。
- 第二优先级:交通标志、信号灯、路沿、障碍物。这些是语义辅助信息,边界质量要求相对低一些。
- 第三优先级:天空、建筑、植被等场景背景。它们不是驾驶决策的核心,但如果完全不区分,会影响模型对“可行驶区域”的上下文理解。
类别之间要有互斥性,不能出现语义重叠。比如“路面”和“车道线”是有交集的——车道线画在路面上。解决方式是把类别做成层级:要么把车道线单独作为一类,同时路面排除车道线区域;要么做多任务输出,路面单独一个head,车道线再一个head。我在实际实验里采用了后者,因为车道线在传统分割输出里只有一两层像素宽度,类别不平衡非常严重,单独分支处理效果更稳定。
3.2 数据来源和样本分布
公开数据集是个很好的起点,但直接拿来训自动驾驶模型会导致国内路况水土不服。公开数据集中以国外城市为主,道路结构、交通标识风格和国内差异不小。可行做法是先用公开数据做预训练,再加入自采数据进行微调。
自采数据的重点是覆盖场景的多样性:晴天、阴天、雨天、夜晚、逆光、隧道出入口、高架桥下阴影区,这些光照条件对分割精度的影响往往比模型结构还要大。我试过在一个模型里加入部分雨夜数据,mIoU整体只提升了一点,但雨夜样本上的单独指标提升了将近10个百分点。这个提升完全不是模型变聪明了,而是见到了原本没见过的那类样本。
数据增强方面,常规的水平翻转、随机裁剪、色彩抖动肯定要做。这里说一个容易忽略的点:分割标签是像素级的,所有几何变换必须同步作用于图像和标签。如果你用了随机旋转,标签图也必须旋转相同的角度和中心点,否则错位标签会直接破坏边界训练。缩放同理。代码里最好把图像和标签放进同一个变换函数里处理,避免出现“标签漂移”。
4. 训练损失函数和采样策略里的坑
模型结构定了,数据准备好了,接下来就是训练。这一步看着简单,实际上新手最容易炸,尤其是损失函数的选择和类别不平衡的处理。
4.1 交叉熵损失在分割里的局限性
最常见的分割损失是逐像素交叉熵,表示如下:
[ L = -\sum_{i} y_i \log(p_i) ]
其中( y_i )是当前像素的one-hot标签,( p_i )是模型预测概率。对小类别比较友好,梯度计算也简单。但自动驾驶场景大多数像素属于“路面”“建筑”这种大类别,而“行人”“摩托车”占比很小。直接用交叉熵,模型会把大量学习能力花在重复学习路面纹理上,小目标准确率很难提上去。
解决思路是加类别权重。给频率低的类别分配更高的损失权重,可以显著改善小类别分割效果。但权重不能拍脑袋定,我会按像素频率的倒数来做一版基础权重,再根据验证集表现手动调整。常见调法:背景类和路面类权重压到0.5左右,车道线、行人、骑行者权重放到2到5之间。
4.2 Dice损失和Focal损失的实际表现
另一个常用损失是Dice损失,它直接优化预测区域和标注区域的交并比:
[ Dice = \frac{2 |X \cap Y|}{|X| + |Y|} ]
Dice损失对小目标和类别不平衡很友好,因为它衡量的是区域重叠程度,不受像素比例影响。但它有个缺点:训练初期如果预测全是背景,梯度会比较平,收敛变慢。
我在实验里的做法是混合损失:主体用带权重的交叉熵,辅助用Dice损失,按照大概7:3的比例融合。这个组合在大多数场景里表现均衡。如果某个任务中小目标特别重要,比如行人检测极端不平衡,可以把Focal损失加进来,对难分样本施加更大的梯度权重,但需要注意学习率要相应降低,否则训练容易震荡。
4.3 采样策略和超参数
Batch size直接影响分割网络的BN层统计效果。显存有限时我最常用Batch size 8配合SyncBN,效果比单卡BN稳定。初始学习率按类别数乘以一个缩放系数来估,比如在ImageNet预训练的backbone上,一般从1e-4到3e-4起步,配合Cosine退火策略,收敛效果比Step Decay更平滑。
还有一个容易被忽略的坑:类别采样不均衡。如果数据集中某类样本的图片数量很少,即使做了像素级权重,也很难扭转。代码里可以先统计每张图包含哪个类别,然后按照包含稀有类别的图片做加权采样,保证每个epoch里稀有类别都能被模型看到足够次数。
5. 分割推理后的处理问题:概率图还远没到能用的程度
模型训练完了,把测试图喂进去,拿到的是一张和原图等宽的类别概率图。很多人直接取每个像素最大概率的类别作为结果,这就是argmax操作。实话说,直接argmax出来的图,边界往往毛毛糙糙,有些独立小区域还会闪烁跳变,在驾驶场景下一旦传到下游,很容易引发误判。
5.1 置信度过滤和时序平滑
模型输出的概率值本身包含了不确定性信息。在实际应用里,我会对每个像素的最大类别概率做一个阈值判断,低于阈值的区域标记为“未知”,不让它参与后续决策。这对夜晚和雨天的低可见度场景特别有用,与其让模型瞎猜,不如诚实地说“这里不确定”。阈值一般设在0.6到0.8之间,太高会漏检,太低会让误检混进来。
时序平滑是另一个重要手段。自动驾驶看的不是单帧,而是一个视频流。把连续帧的分割结果按像素做指数滑动平均,可以显著减少类别跳变:
[ Score_t = \alpha \cdot Score_{t-1} + (1 - \alpha) \cdot Score_t ]
(\alpha)取0.7到0.9之间,既能过滤噪声,又不会让响应变得太迟钝。我第一次调时序平滑参数时太贪心,把(\alpha)设到0.95,结果车辆都压线了输出还没跟上,这提醒我:平滑参数要和实际控制频率一起调。
5.2 连通域过滤和边界修正
分割结果里常见的噪声是孤立小区域。比如路面上偶发的一小块“行人”误检,或者骑车人的轮胎被分割成断断续续的几段。解决办法是先做连通域分析,把面积小于阈值的连通区域剔除或者重标为主要背景。面积阈值根据任务和图像分辨率调整,在1080P图像上我一般把低于20像素的区域直接滤掉。
边界修正方面,常用手段是条件随机场。无需研究太深的数学原理,它的思路是结合像素自身的类别概率和相邻像素的颜色相似性,让颜色接近的像素倾向分到同一类别,从而把锯齿状边界修平滑。实测对低分辨率输入尤其明显。在GPU推理上有一些加速实现,但CPU上跑起来会比较慢,是否引入要结合延迟预算决定。
6. 从实验室Demo到实车部署的三道门槛
训练阶段跑得再漂亮,到了部署环节也会被现实教训。这里说三道我实际跨过去的门槛,每一道都值得专门做一轮工程优化。
6.1 算力和延迟约束下的模型压缩
实验室用的GPU卡在车端通常不存在。车规级计算平台的算力可能是实验室GPU的十分之一甚至更低,而且有严格的功耗上限。直接部署原始Unet基本不可能实时。
我实际走的路线是先做通道剪枝,把不重要的卷积通道剪掉,通常可以压掉30%到40%的FLOPs,精度损失控制在2个百分点以内。然后是量化,从FP32降到INT8,推理速度能再提升一倍左右。量化后常见问题是边界区域出现轻微噪点,我的处理方式是量化后重新用少量精标数据做一轮微调,把量化误差抵消掉一部分。知识蒸馏也可以考虑,用大模型当Teacher指导小模型,但在时间紧的项目里,剪枝加量化加微调已经足够。
6.2 多线程和帧率抖动
分割模型在嵌入式平台上推理时间不稳定,时快时慢,这个比绝对延迟还致命。原因在于动态内存分配和缓存污染。优化手段包括:锁页内存池避免频繁分配、把输入图像分辨率固定下来避免动态shape、CPU绑核避免调度抖动。我遇到过最夸张的情况:同一模型在设备上帧率从15毫秒突然跳到35毫秒,最后定位到是动态库加载时触发的中断处理,加锁和预热之后才稳定住。
6.3 数据漂移和闭环验证
模型上车后还会遇到训练时没见过的数据分布。光照角度变了、城市换了、季节换了,精度就往下掉。这需要建设数据回流通道:把实车采集的失败case自动抽帧、筛选、送标注、再训练。听起来像繁重工程,但哪怕先做一个最简版——每天固定导出一批模型分错的图片,人工确认后加入训练集——也能明显延缓模型退化速度。
7. 关于分割模型效果的评价和提升
最后这一点想专门说一说。做算法的人习惯用mIoU、mPA这类指标衡量分割效果,但我做了这个项目后越来越觉得,单一指标会骗人。
7.1 从mIoU看问题
mIoU是把每个类别的交并比做平均,数值高不代表驾驶场景下真的好用。一个模型可能在“路面”这类大区域上表现极好,把mIoU拉得很高,但“行人”类别交错严重,真实驾驶中根本不敢用。看指标的时候不光看平均值,还要逐类看:
- 可行驶区域边界上的误分像素数量
- 行人/骑行者类别的单独IoU
- 小物体是否稳定出现在连续帧里
7.2 指标之外需要自己画图看
我每次训练迭代完都会挑一批典型场景保存分割可视化结果,包括直道、路口、夜晚、雨天、隧道口。肉眼观察的重点是:路沿是否连续、远处物体是否被稀碎地误检、车辆底部影子是否误导模型判断。这种主观检查能发现mIoU完全反映不出来的问题。有一次模型mIoU在验证集上提升了一个点,但可视化图中所有自来水管都被分割成了“栏杆”,这种错误在数值指标里损失很小,却会在实际系统中带来明显安全隐患。
7.3 模型迭代的经验
分割模型的迭代闭环应该是:训练后先可视化,再逐类统计指标,再回到数据层面补充难例。不要一上来就换网络结构。我见过太多团队在一个不成熟的数据集上反复试几个backbone,最后发现加了3000张困难样本,效果比换任何大模型都明显。模型结构的事,放在数据和工程问题解决之后再做决策不迟。
到此为止,这个项目从原理到实操就讲得差不多了。如果让我用一句话总结这段时间的经验:自动驾驶分割不是比谁的模型更大更花哨,而是比谁对数据更诚实、对场景更敏感、对部署约束更清醒。希望这篇分享能让你少走一点我走过的弯路。