自动驾驶可行驶区域检测实战:从语义分割到工程落地
我之前被问过一个问题车道线都画得清清楚楚了为什么还要多此一举去检测“可行驶区域”这个问题的答案恰好是自动驾驶感知模块里最容易忽视、又最能决定行驶安全的部分。自动驾驶车道可行驶区域检测解决的不是“车在哪”而是“这辆车能往哪里开”——它把车辆前方所有可通行的路面像素找出来输出成一张像素级掩码图。车道线检测告诉你哪里有“线”可行驶区域检测告诉你哪里有“面”。这篇文章把我在这个方向上从方案选型、数据准备、模型训练到部署优化的完整实践过程梳理一遍重点讲踩过的坑和复盘出来的经验。内容不追求教科书式的全面而是尽量贴近真实工程链路适合正在做车载感知、准备入门语义分割落地或者想搞懂这个方向核心门道的同学。1. 先说清楚我们到底要检测什么1.1 可行驶区域和车道线检测根本不是一回事刚开始接触这个方向时我犯过一个理解上的错误——以为可行驶区域检测就是车道线检测的“加强版”。实际做下来发现这是两套完全不同的任务逻辑。车道线检测输出的是“线条”车道的左右边界、虚实线告诉车辆的是结构化信息。可行驶区域检测输出的是“面”一张和输入图像同尺寸的掩码图每个像素被标记为“可行驶”或“不可行驶”。用一个不太严谨但好懂的说法车道线是路面的“骨架”可行驶区域是路面的“肌肉”只知道骨架还不够必须连肌肉边界一起估出来。从输入输出的维度看车道线检测很多方法输出的是稀疏的“线”或“点”用多项式拟合就能表达可行驶区域检测是稠密的像素级输出每一个像素都要给出分类结果。这个差异直接决定了标注方式、模型结构、后处理复杂度完全不同。从应用场景看在标准高速公路上车道线清晰、道路边界明确车道线检测基本够用。但到了城市道路、无车道线路口、施工围挡区域、雨雪覆盖的车道线上车道线信息直接失效车辆不能“瞎猜”。这时候可行驶区域检测就承担起兜底职责不管线在不在只要是路沿或道路边界内的可通行像素都要识别出来。我现在给团队新人讲这个项目时第一条就强调别把这两个任务混在一起。它们是感知链路里互补的输出不是一个任务的两个变体。1.2 它在自动驾驶感知链路中的位置自动驾驶感知模块通常由几类任务组成静态交通环境感知车道线、可行驶区域、路沿、动态目标感知车辆、行人、障碍物、自车状态感知定位、速度。可行驶区域检测属于静态环境感知里最重要的一个维度它给下游的预测和规划模块提供了一块“允许区域”约束。规划模块做轨迹决策时本质上是在“几何可行域”里求一条最优路径。这个几何可行域从哪来很大一部分来自可行驶区域检测的输出。假设前面没有车道线但有一大片平整路面周围是路沿石规划模块如果只知道车道线不存在会变得束手束脚。可行驶区域检测输出前方有一条连续可通行带后规划模块就拥有了一个天然轨迹走廊。如果中间还有障碍物预测模块再把障碍物占据空间扣掉剩下的区域就是真正可规划空间。这个链条也解释了为什么可行驶区域检测值得单独做它承担着一层“自由空间估计”的功能是很多下游模块共同依赖的基础。1.3 真实场景带来的挑战我最早做这个方向时拿到的都是“干净”数据集——晴天、光线充足、路面平整。等真正上车跑起来才发现现实远比数据丰富。光照突变进出隧道、树荫、逆光同一块路面的亮度差异巨大。天气与路面状态雨天反光、积水倒影、雪天覆盖直接改变道路观感。路面结构差异老沥青、混凝土、碎石路颜色纹理各不相同。视角变化摄像头装在挡风玻璃后方与地面存在明显俯仰角远处信息分辨率极低。动态遮挡前车车身阴影盖住路肩造成“可行驶区域被切断”的假象。任何一个因素单独拎出来都能让一个只拟合过训练集分布的模型“翻车”。这也是为什么这个方向在行业里很少有人能做“一版定稿”——它不只是把模型调通而是要把上述问题逐个啃掉并且保证在任意一帧上都不出现致命误判。我把这个项目称作“奇妙之旅”原因也在这里你永远不知道下一个路口、下一块路面上藏着哪个没被发现的失败模式。2. 方案选型为什么最后走上语义分割这条路2.1 传统图像处理方案卡在哪里最早接触这个任务时第一反应是用经典计算机视觉方法灰度化、边缘检测、霍夫变换提取直线再根据直线围成的区域判断可行驶区域。这个思路在结构化道路的仿真视频里效果不错但一上真实道路就各种问题。边缘检测对手感太敏感。阴影交界处、柏油裂缝、前车轮廓都可能被当成道路边界。光照一变化同样的路面在不同帧里的灰度分布完全不同固定阈值基本失效。更深层的问题是它没有语义理解——不知道“路面那块黑点区域其实是斑马线的一部分”更不知道水洼反射是什么意思。传统方案最大的问题在于它不是学习型的。每个参数都是手工调出来的换个场景就得重新调一遍根本经不起泛化。这个教训我在其他感知方向上也碰到过最终都不可持续。2.2 语义分割是自然的选择可行驶区域检测本质上是一个逐像素分类问题语义分割天然就是干这个事的把图像中每个像素打上类别标签直接输出与输入同尺寸的分割图。它的优势非常明显端到端学习。从原始图像到像素级语义标签中间没有手工提取特征。能利用上下文信息。卷积感受野让模型学会“路沿上方通常是人行道”以及“大面积平坦灰黑色区域大概率是马路”。对不规则边界的表达能力强。弯道、丁字路口、施工导改区都能输出平滑轮廓。方案对比下来我最终选了编码器-解码器结构的语义分割模型而不是检测框或者传统几何方法。检测框表达不了不规则行驶边界——框会浪费大量像素传统几何方法则是把“判断哪里能开”拆成“先找边界再填充内部”误差沿着pipeline逐步累积。语义分割一步到位后续所有优化都可以在“逐像素分类”框架里迭代。2.3 分支方案的取舍选型时我还参考过基于锚点的检测方法和Transformer类分割方法。基于锚点的方法在主流的车道线检测任务里很流行因为它们输出结构化结果可以直接输入控制模块。但可行驶区域边界不服从“锚点”假设——丁字路口的可行驶区域任何固定形状的框都框不准。Transformer类方法在公开榜单上精度很高但计算开销大在边缘设备上的实时性很难保证初期不建议作为落地主力。所以最终核心思路非常清晰把可行驶区域检测当作语义分割问题来做用编码器-解码器结构输出逐像素概率图。3. 数据是硬骨头标注、采集与增强3.1 选数据集第一条经验不要迷信公开榜一开始我也走了下载公开数据集的路线。公开数据集标注质量确实好但有一个问题它的相机内参、安装角度、场景分布跟自车的实际配置往往不一致。模型在公开集上的mIoU再高换到自车摄像头视角下也可能掉点十几个百分点。正确做法是“公开数据预训练 自采数据微调”。先用公开数据把模型的基础能力训出来再拿自车摄像头采集的数据做微调让模型适配实际施工场景。如果预算紧张至少要采集几百帧与目标场景同一分布的数据做测试集用来观察泛化能力。3.2 标注规范边界怎么定直接决定模型上限可行驶区域标注细节看着简单实际容易“打架”的地方特别多。我们早期踩过最深的坑就是“可行驶区域”和“道路区域”没分清。什么是可行驶严格来说当前车辆能通过且合法的区域才能算“可行驶”。什么不算人行道一般不算路沿石以上的草坪不算。被前车挡住的路面怎么标如果只标可见部分车辆阴影下的路面就容易漏标模型会学到“车底下没有路”的错误印象。被积水覆盖的区域怎么标真值应按物理路面标不能按视觉反射区域标。实践之后我们制定了一套标注规范文档把每个边界情况都定义清楚车辆阴影下的可行驶区域仍然标为可行驶积水区仍然标为可行驶施工围挡外侧不能标有明显高度差的路沿不算可行驶。每条规则都配了示例图标注员按图比对操作不一致率从早期的10%以上降到了2%左右。别小看这一步标注质量直接决定模型收敛的上限。3.3 类不平衡问题两个类也会失衡理论上二分类问题每个像素不是0就是1好像不会失衡。但实际自动驾驶画面里可行驶区域往往占图像60%~80%不可行驶区域占20%~40%交叉熵损失在这种分布下会把大类别学得很好小类别边界反而容易被忽略。处理办法分三个层次数据层面采用裁剪策略把包含高比例“不可行驶”区域的图像块裁出来补充训练。损失层面在交叉熵基础上叠加Dice损失或边界损失让训练更关注两个类别的交叠区域。采样层面对困难样本做在线挖掘优先用loss最大的像素更新。三个层次可以叠加使用。我实际跑下来Dice损失带来的边界改善比单纯换一个更大模型的收益还要明显。3.4 数据增强不是越多越好数据增强能提升泛化能力但不能无脑堆。我在这个项目里试过大量增强组合最终保留的是亮度/对比度扰动、随机水平翻转、随机裁剪/缩放、少量模糊和噪声。这些增强分别模拟了进出隧道、树荫、左右对称场景、远处低分辨率以及雨天和摄像头起雾的情况。没保留的包括随机旋转角度大了会破坏地平线一致性和随机颜色反转会让模型学到“深色等于可行驶”的错误关联。这里有个独门小技巧在增强里加入“自车阴影模拟”在路面上叠一个半透明暗色矩形块模拟自车车头阴影。这个技巧开源项目里很少写但在工程场景里非常有效。4. 模型搭建与训练从复现到调优4.1 编码器-解码器结构怎么选语义分割的可选方案很多从FCN到U-Net、DeepLab系列再到各种轻量化分割网络。如果只追求精度上大模型就行但要落到车端芯片上推理成本必须纳入考量。我实践下来效果不错的一套配置是编码器部分采用轻量级骨干网络EfficientNet-Lite或MobileNetV3这类结构在速度和精度之间取平衡。解码器部分引入浅层特征与深层特征融合的结构思路类似U-Net的skip连接。具体做法是把骨干网络倒数第四层、倒数第二层和最后一层特征分别上采样然后拼接在一起用逐层卷积调整通道最后通过1x1卷积输出2通道的logits。输出通过Softmax得到每个像素的概率再按阈值生成二值掩码。这个结构的好处是既能学到低层的边缘纹理信息对路沿和积水边界至关重要又能利用高层的语义信息判断“这块区域是路面”。我实测过只用最后一层特征直接上采样输出边界锯齿非常严重加了skip连接之后边界质量明显提升mIoU能高出约5个百分点。还有一个常被忽略的细节输入分辨率。车载摄像头原图通常是1080p甚至更高但模型输入不必这么大。我采用的训练分辨率是640x384兼顾计算量和远处道路覆盖。算力允许时提高到800x448但并非越高越好——太高会增加过拟合风险太低则远处区域根本无法判断。4.2 损失函数别只盯着交叉熵这个项目的损失函数设计是控制性能提升空间最大的地方。初期我只用交叉熵损失训练出来的模型能看但边界歪歪扭扭路沿附近的像素经常被一笔带过。后来迭代了两步。第一步叠加Dice损失它衡量预测区域和真实区域的“重叠度”对前景和背景都关心对小区域更敏感。叠加之后边界精度提升明显模型更愿意把“一小片完整路面”预测出来而不是只预测中间部分。第二步引入边界损失让网络更关注靠近真实边界的像素。这一步解决的是路沿连续性问题——断线现象明显减少。最终的损失函数一般写成import torch import torch.nn.functional as F def dice_loss(pred, target, smooth1.0): # pred: [N, 2, H, W] logitstarget: [N, H, W] 标签 pred torch.softmax(pred, dim1)[:, 1, :, :] pred pred.contiguous().view(-1) target target.contiguous().view(-1).float() intersection (pred * target).sum() return 1 - (2.0 * intersection smooth) / (pred.sum() target.sum() smooth) ce_loss F.cross_entropy(logits, labels, ignore_index255) loss ce_loss dice_loss(logits, labels)要注意在二分类语义分割里把任务当成“多分类”用交叉熵没问题但一定要确认标签里是否设置了ignore_index。如果背景里有不想参与训练的区域比如图像边缘畸变区、动态车辆遮挡区要在标注阶段给一个ignore标签并在损失计算时掩掉否则模型会被“这辆车底下到底算不算路”这类噪声标签干扰。4.3 训练配置与参数调整经验以下是一套可复现的训练参数直接抄作业能少走不少弯路优化器AdamW比Adam更稳配合余弦退火学习率调度效果最好。初始学习率batch size为16、输入640x384时建议从2e-4开始。batch size减半时学习率也应相应减半。训练轮数纯自采数据从零训练需要30到50个epoch公开数据预训练后微调一般10个epoch就稳定了。混合精度训练使用AMP可以把显存占用和训练时间都降约一半精度损失基本可以忽略。训练过程中必须盯着几条曲线训练损失、验证损失、mIoU以及特定场景下的可视化预测图。只盯mIoU不看图片很容易错过“指标还行但边界一团糟”的问题。我当时养成了一个习惯每2个epoch固定用同一批验证视频帧做推理可视化并存成拼图对比。这个方法让我能直观看到“第8个epoch边界变差了但mIoU没变”这类现象大幅缩短debug时间。4.4 过拟合与泛化怎么判断模型真的“会开车”模型在训练集上mIoU很高不代表换一个城市、换一种天气还好用。这个项目的终极检验标准是“没见过的道路”。我会刻意把测试集设计成时间上完全不同步、地点也错开的片段防止时序重叠造成数据泄露。如果只在某一个城市采集数据模型很容易学到这个城市的特殊标记比如某个区域的柏油路颜色比例。这种城市级过拟合非常隐蔽因为验证集看起来一切正常。解决办法有三个方向增加不同城市的采集数据引入仿真软件渲染的合成数据以及做更强的色彩抖动来消解颜色偏好。另外可行驶区域检测和车道线检测共享骨干网络在理论上是可行的能节省算力这也是工程里常用的多任务学习思路。但如果两个任务的数据分布差异过大共享骨干可能互相拖后腿。这事必须实验验证不能想当然。5. 工程落地延迟、量化与后处理5.1 帧率预算与延迟拆分自动驾驶场景对感知延迟要求很苛刻。这里假设目标是把端到端感知延迟控制在60到80毫秒以内其中留给可行驶区域检测模型的推理时间约40毫秒以内。部署前需要先做延迟预算拆解图像预处理缩放、归一化、通道转换约3到5毫秒模型前向推理30到40毫秒后处理上采样、阈值化、轻量形态学约2到5毫秒如果模型推理时间超预算先不要急着砍精度按三个顺序排查是否存在无意义的等待耗时比如推理和前处理是否串行阻塞是否有多余的中间算子比如半精度转换和Resize反复触发输入分辨率是否过大。640x384和800x448之间常常有20%以上的推理时间差。5.2 量化与蒸馏工程降本最实在的路径模型量化是落地必须走的一步。把FP32模型转成INT8推理时间通常能快1.5到3倍显存占用也更低。但量化会带来精度损失尤其在边界细碎的像素上可能出现碎点闪烁。处理量化精度损失我的步骤是先做训练后量化PTQ用500到1000张代表性图片校准记录量化前后的mIoU差如果掉点超过3个百分点就改用量化感知训练QAT用带量化噪声的训练流程微调几个epoch对量化后边界闪烁在后处理里加一个小窗口的时序一致性滤波对连续帧的同位置结果做滑动平均。模型蒸馏也有奇效用高精度大模型作为teacher输出软标签训练轻量小模型student。同等推理速度下蒸馏出来的student模型能比直接剪枝训练高一到两个点的mIoU。蒸馏时teacher和student的数据增强策略可以不同student需要更强的正则化。5.3 后处理小细节大作用很多人把后处理想得很简单——拿到概率图阈值0.5转掩码完事。实际上落地时后处理对体验影响很大。我常用且安全的后处理包括三大件双线性上采样回原图分辨率。直接在float概率上用双线性插值比先转二值再插值边缘更平滑。轻量形态学操作。先腐蚀后膨胀去掉离散噪声点再膨胀一次填补小孔洞。核大小别超过3x3或5x5否则容易糊掉真实障碍物边缘。连通域最小面积过滤。把面积小于阈值的独立区域直接丢弃避免“树影被识别成可行驶区域补丁”这类误检。这个方法看着简单实际对远处影子误检特别有效。还有一个容易被忽略的问题时序一致性。单帧输出偶尔抖动正常但车连续行驶时相邻帧的可行驶区域边界应当平滑变化。我在部署时加了一个轻量级时序平滑池保存最近5帧的掩码对每个像素做多数投票。实现成本极低稳定性和视觉效果提升非常明显。但要注意时序滤波会增加几毫秒延迟也会在极端情况上一帧没检测到这一帧突然出现障碍物下让输出滞后。开始做这个优化之前先确认你的规划模块对延迟的敏感程度不能无脑加。6. 常见问题与排查技巧实录6.1 训练震荡不收敛现象是训练损失下降到一定程度后剧烈震荡验证mIoU上不去。常见原因有三个学习率太大或batch size太小标签中存在大量噪声样本损失中Dice部分梯度不稳定。排查步骤建议如下先把Dice损失去掉只跑交叉熵看能否收敛如果交叉熵也不稳定把学习率降到1e-4以下检查数据加载的shuffle逻辑是否正常——有些新手的增强逻辑里随机翻转和shuffle叠加使用导致每个epoch数据分布剧烈变化把震荡误判成学习率问题。6.2 远处区域漏检现象是近距离路面检测效果好远距离大量漏检。原因在于车载摄像头对远处路面占比很小远处像素分辨率低同时很多标注规范把远处区域标成“忽略”模型根本没有得到监督信号。解决办法有四招把输入分辨率提高到800x448以上在损失里引入距离加权让网络更关注远处辅助增加一个“远景掩码分支”用远距离标注单独监督检查数据采集时是否包含了足够多的远处道路片段。6.3 边界锯齿严重现象是预测掩码边缘毛刺多路沿断断续续。原因主要是模型输出分辨率不足、后处理缺少平滑、损失函数对边界不敏感。解决办法使用Dice或边界损失在解码器上采样时使用双线性而不是最近邻后处理加3x3中值滤波在高分辨率特征图上额外监督一个“边界头”。我的经验是想要“看起来像人标的那样”边界损失比单纯放大模型结构更直接因为训练目标本身就对准了边界定位。6.4 常见问题速查表下面这份速查表覆盖了我在这个项目里遇到的大部分问题足够处理80%的日常排查。问题现象可能原因优先排查项推荐解法训练损失居高不下标签错误或增强过强抽查标注可视化结果修正标注规范降低增强强度预测掩码全为背景模型未收敛或学习率过低查看训练曲线调高学习率或增大batch size预测掩码全为前景类别严重失衡或阈值太低输出概率分布统计调高阈值或使用加权交叉熵边界锯齿严重分辨率不足或缺少边界监督查看可视化样本加Dice/边界损失加后处理雨天夜间掉点数据分布偏差统计数据集场景分布增加对应场景训练样本INT8量化后掉点明显校准集代表性不足换校准集重新测试改用QAT或降低量化粒度连续帧闪烁明显单帧推理噪声查看帧间输出一致性加时序一致性滤波全图输出模糊输入分辨率过低或原图失焦检查原图质量增大输入分辨率检查图像预处理6.5 三个容易忽略的经验关于这个项目有几个我预期之外的发现看完能帮你节省不少时间。第一模型对“路面颜色”的依赖远超想象。在晴天柏油路上训练出来的模型换到水泥路上往往掉点因为模型学到的关键特征不是“纹理像路”而是“颜色像路”。所以数据增强阶段一定要做色彩抖动不能只在亮度上做文章。第二减少参数量对语义分割结果的影响往往比增加数据量更容易处理。我在用轻量backbone替代大backbone时发现只要把Dice和边界损失调好两者mIoU差距只在1到2个点左右。这说明在这个任务里损失函数和训练技巧比模型体积更值钱。第三后处理加入“最小连通域过滤”看似多余实际对消除“远处车辆影子被识别成可行驶区域补丁”这类误检极其有效。我最初没做这一步时每帧都会出现零星误检块规划模块频繁跳变。加了这个过滤后整体稳定性明显提升代码量不过十行左右。回头再看整个项目我最想分享的实践体会是可行驶区域检测表面上看是“输出一张掩码图”的简单任务实际是数据、模型、损失、部署四层工程都要配齐才能做好的复杂系统。很多人在复现公开模型时觉得简单一旦走完整条落地链路就会发现每个环节都藏着需要踩平的坑。这个方向后续还能扩展很多把可行驶区域检测和障碍物检测合并进统一的多任务感知框架减少重复计算用自监督预训练降低对大量标注的依赖把BEV视角纳入检测输出让下游规划模块直接拿到俯视自由空间。如果你正在做或准备做类似方向我的建议是先别急着堆模型复杂度把数据集做干净把标注规范捋清楚把损失函数调明白。这些“看起来很基础”的工作恰恰是决定模型最终上限的地方。