十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

YOLO多任务道路识别实战:检测车道线可行驶区域一体化模型

YOLO多任务道路识别实战:检测车道线可行驶区域一体化模型 简介面向计算机视觉初学者的YOLO多任务道路识别项目代码包围绕YOLOv5、YOLOv8、YOLOv11系列模型解决复杂道路环境下车辆、斑马线、交通标志、路灯等7类目标的实时检测问题。资源包共46个文件压缩后仅115KB以txt配置与说明、jpg样本图像、yaml数据集配置、Python训练/演示脚本为主要构成其中txt便于查看标注类别与使用说明jpg直观展示道路场景样本配置文件和脚本分别承接训练配置和检测流程另外附带检测结果示例图与网页展示页目录结构清晰便于按需查阅。目前已有104人学习下载。使用者可基于CULane数据集的处理思路对照labelimg标注流程和YAML配置写法快速复现并扩展其中任意一种YOLO版本的道路目标检测Demo从而掌握从环境搭建到自定义数据集训练的关键环节。这份代码将理论讲解落地为可直接运行的项目骨架适合作为入门目标检测、开展课设或毕设项目的参考起点。 前阵子同事丢给我一个需求一套道路识别系统要能同时输出车辆行人检测框、车道线、可行驶区域。我查了一圈资料之后锁定了YOLO多任务道路识别这条路线也就是在同一个模型里把目标检测和分割任务合并成一条推理链路。跟传统三个模型拼装的方案比这套YOLO多任务项目代码的部署体积、推理延迟都明显占优而且训练调试的坑相对集中我前后用两周时间就把第一版跑通了。这篇博文就是整个过程的完整复盘从模型分支结构、数据集转换脚本到训练参数和踩坑记录全部整理出来适合准备做自动驾驶感知、智慧交通监控或者毕业设计想切入道路识别方向的同学直接参考。1. 为什么道路识别必须走多任务路线1.1 路上要同时看到的东西太多了很多初次接触道路识别的人有个错觉道路识别就是找车。真正落地时你会发现一套可用的道路感知系统至少包含四类信息车辆和行人检测框、车道线线路分割、可行驶区域区域分割以及交通标志小目标检测。这些信息是互相配合的比如判断当前车道能不能变道既要看车道线在哪又要看周围车辆距离还要知道可行驶区域是否被占用。如果每个任务都单独跑一个模型就需要维护多个前向推理内存和算力都是成倍开销。1.2 多任务共享特征到底省在哪里我最早也用过纯拼接方案YOLOv8做检测再加一个分割网络做车道线。实测下来两个模型的参数量加起来接近一个多任务模型的两倍而且推理时需要排队执行延迟直接翻倍。多任务模型的核心逻辑是让Backbone和特征金字塔完全共享因为检测和分割依赖的特征其实是高度重叠的边缘、纹理、目标位置、空间关系。一个Backbone提完特征三个任务头各取所需最后再把loss合在一起反传。不过多任务也不是没有代价。最典型的问题是特征冲突检测头更关注这个区域里面有个什么东西分割头更关注这个像素属于哪个类别。这两个任务对高层语义特征的要求并不是完全一致的。我后面通过调整不同任务头的特征图接入层来缓解这个细节放到第2章细说。总之结论很明确在道路识别这种强相关性任务集合里多任务带来的收益远大于它引入的麻烦这也是我用YOLO作为基座而不是另起炉灶的原因。2. 项目代码结构一个Backbone三个任务头2.1 整体模型我们是怎么设计的我用的基础框架是YOLOv8的CSPDarknet结构但去掉官方的单检测头改成三路输出。输入图像统一缩放成640x640Backbone产出三层特征P3、P4、P5分辨率分别是80x80、40x40、20x20。这些特征先进入PAN-FPN做跨尺度融合融合之后分给两个方向检测头在前分割头在后。具体划分是这样的DetectHead复用YOLOv8的anchor-free检测头负责预测车辆、行人、交通标志的类别和边框。LaneHead接P3和P4特征上采样到640x640预测每个像素是不是车道线。DrivableHead同样上采样到640x640预测每个像素是否属于可行驶区域。两个分割头虽然结构类似但LaneHead的输入特征偏向浅层的P3细节因为车道线是细长结构深层特征经过多次下采样后细节丢得厉害。可行驶区域则更多依赖语义信息P4和P5的高层特征更关键。这个差异是调试过程中用特征可视化发现的也是我自定义这个多任务结构而不是直接套用现成代码的主要原因。2.2 项目代码目录与核心前向逻辑整个项目的代码结构尽量保持简洁方便移植到自己工程里yolo_multitask_road/ ├── config/ │ ├── multitask.yaml # 模型结构配置 │ └── train.yaml # 训练超参数 ├── datasets/ │ ├── bdd2yolo.py # BDD100K转YOLO格式脚本 │ └── augment.py # 多任务数据增强 ├── model/ │ ├── backbone.py # CSPDarknet │ ├── neck.py # PAN-FPN │ ├── head_det.py # 检测头 │ ├── head_seg.py # 车道线可行驶区域分割头 │ └── yolo_multitask.py # 多任务模型组装 ├── train.py # 训练入口 ├── val.py # 验证脚本 └── demo.py # 推理可视化核心的多任务前向逻辑在yolo_multitask.py里关键代码大致是这个样子class YOLOMultiTask(nn.Module): def __init__(self, cfg): super().__init__() self.backbone CSPDarknet(cfg) self.neck PANFPN() self.det_head DetectHead() self.lane_head SegHead(in_channels128, out_channels1) self.drivable_head SegHead(in_channels128, out_channels1) def forward(self, x): p3, p4, p5 self.backbone(x) neck_out self.neck([p3, p4, p5]) det_out self.det_head(neck_out) lane_out self.lane_head(neck_out[0]) # 浅层细节特征 drivable_out self.drivable_head(neck_out[1]) # 融合语义特征 return det_out, lane_out, drivable_out注意这里的分割头输入用了neck输出的特征组合而不是直接把backbone的P3单层接进去。因为neck已经做了跨尺度融合检测和分割都能受益。实际训练下来这种设计让车道线的连续性比直接用P3好不少。3. 数据集准备从BDD100K到YOLO多任务格式3.1 多任务训练绕不开的数据集选择道路识别界最常用的开源数据集是BDD100K自带10万张真实道路图像并且同时包含检测框、车道线、可行驶区域三套标注。Cityscapes虽然画质好但只有5000张精细标注数量上差一个量级而且集中在德国城市泛化到国内交通场景差点意思。ApolloScape也是好数据但标注复杂度和获取门槛比BDD100K高。综合考虑之后我选了BDD100K。下载的时候注意只需要三个子集detection、lane marking、drivable area。为了省时间第一次跑可以先只拿train的3万张子集数量完全够训练出一个能看效果的多任务模型等到正式调参再用全量。热词里提到的visdrone2019转YOLO也是类似思路不同之处在于visdrone是无人机视角如果做俯视交通监控可以套用相同的转换脚本逻辑。3.2 标签格式转换脚本的几个关键点BDD100K的检测标注是JSON格式而YOLO训练需要txt文本每行是类别ID cx cy w h的归一化坐标。转换脚本要处理的最大坑是BDD100K的坐标单位是像素必须除以图像宽高做归一化类别ID还要重新映射成从0开始的连续数字否则训练时类别索引会越界。分割标签处理起来问题更多。BDD100K的车道线和可行驶区域用的是不同颜色编码的png图片转换时不是简单的二值化因为背景、车道线、路沿这些对象在png里各有不同像素值。我的处理方式是把原图中车道线对应的pixel class抽出来生成一张二值掩码可行驶区域同理然后再统一resize到640x640。转换脚本的核心部分可以抽象成这样的逻辑def convert_seg_mask(src_png, target, anchor_id): mask (src_png anchor_id).astype(np.uint8) mask cv2.resize(mask, (640, 640), interpolationcv2.INTER_NEAREST) np.save(f{target}/seg_{anchor_id}.npy, mask)车道线anchor_id是0可行驶区域anchor_id是1。resize时用INTER_NEAREST而不是线性插值这一点非常关键线性插值会在车道线边缘产生介于0和1之间的模糊值等于引入了假标签噪声。3.3 样本清洗与数据采样策略数据集不是下载完就能直接用我花了不少时间做清洗。BDD100K里有一部分夜间、雨天样本虽然训练需要这些增强鲁棒性但占比过高会拖累白天场景的精度。我的策略是把白天和夜晚样本按大约7比3的比例混入训练集。另外有些图像极其相似比如同一路段连续帧直接全量训练会过度拟合到特定路段。我用图像hash去重把过于相似的帧筛掉一部分训练收敛速度立刻快了一截。分割任务还有一个困扰新手的问题可行驶区域在整张图里占比非常大而车道线占比非常小如果不做任何处理模型很容易把全部像素都预测成背景loss看起来也在下降实际上分割头完全废了。这个问题我放到下一章的损失函数环节专门解决但数据阶段也可以先做一件事对车道线图像做随机裁剪增强让模型多看到局部车道线。4. 训练配置、损失函数与调参干货4.1 三个任务头的损失是怎么算的训练一个多任务模型最忌讳的就是把三个任务的loss直接相加。不同任务的loss数值范围差异巨大检测头包含分类loss和回归loss动辄几十的数值分割loss用交叉熵的话也接近几十如果直接相加梯度会被数值大的任务主导另一个任务几乎学不到东西。我的做法是给每个任务设定独立的权重系数总损失定义为loss (lambda_det * loss_det lambda_lane * loss_lane lambda_drivable * loss_drivable)初始权重是lambda_det0.5lambda_lane0.3lambda_drivable0.2。三个loss里检测沿用CIoU加BCE分割则用了BCE加Dice的组合。车道线的正负样本比例极端悬殊Dice loss对这种不平衡不敏感能强制模型关注到细长的车道线区域实测加上Dice之后车道线的mIoU从40边缘直接拉到58左右。可行驶区域的loss相对平静因为背景和可行驶区域的比例没有车道线那么极端BCE就能较好工作。训练过程中我建议记录三个loss数值的变化曲线观察是否存在某个任务loss长期不动如果发生多半是权重分配或者梯度方向出了问题需要降低该任务权重或者调整学习率。4.2 从COCO预训练到道路识别的迁移技巧直接用随机初始化训练多任务模型收敛速度非常慢前几十个epoch都在原地打转。我用的是COCO预训练的YOLOv8权重来做Backbone初始化加载的时候固定跳过检测头的权重复制因为我们的检测头类别数跟COCO的80类不一样。python train.py --data config/train.yaml --epochs 300 --batch-size 16 --device 0 --pretrained yolov8s.ptbatch-size方面如果显存不够优先降低分割头分支的通道数而不是缩小输入分辨率。输入分辨率从640降到512检测精度掉得很明显但分割头的通道数从64降到32精度损失有限模型速度还能提升。学习率我习惯用0.01配SGD加余弦退火warmup前3个epoch让预训练权重平稳过渡到多任务训练状态。这里面还有个容易忽略的细节混合精度训练。多任务模型比单任务模型更容易遇到显存问题AMP自动混合精度是标配。我在训练时发现分割头的Dice loss在fp16下数值稳定性比较差偶尔出现NaN最后是把Dice loss计算部分强制回fp32训练就稳定了。4.3 AMD显卡和常见环境问题热词里出现了一个特别现实的问题AMD RX 580这类A卡能不能跑YOLO。答案是可以但环境跟N卡完全不同。AMD显卡需要用ROCm而不是CUDAROCm对PyTorch的支持版本有限制RX 580属于GCN架构在较新版本ROCm里甚至已经被列为legacy支持安装PyTorch时要用对应的ROCm版本号否则跑不起来。我的建议是如果你手头只有AMD老卡优先用Linux环境加ROCm 5.xWindows下的兼容性会让人花掉大量时间。另外RX 580的6GB显存跑YOLOv8s多任务模型batch-size要压到8左右混合精度必须开。要是只有4GB显存就老实换YOLOv8n的Backbone不然训练没法正常推进。环境配置的其他坑集中在onnx导出和部署阶段。导出onnx时多任务模型有多个输出节点onnx默认导出顺序可能跟推理脚本不一致最好在导出时显式指定输出节点名称。部署到TensorRT的话多任务模型的分割头动态shape会比较麻烦第一次构建engine时预热一个固定的640x640输入尺寸避免运行时反复重新优化。4.4 验证指标的选取多任务模型的验证指标不能只看一个。检测部分用mAP0.5和mAP0.5:0.95分割部分用mIoU。很多博文只报mAP那是单任务YOLO的习惯。多任务必须三个指标一起看否则很可能检测涨了、分割崩了。我每5个epoch在验证集上跑一次记录三个指标并保存最优权重而不是简单保存最后一个epoch。验证的时候还有个小坑分割输出要经过阈值处理之后才计算mIoU阈值一般取0.3到0.5之间我最后固定用0.45跟推理阶段保持一致这样验证数字和实际效果才不会出现巨大差异。5. 推理与可视化把三个任务的输出合成一张图5.1 推理流程与后处理模型训好后推理阶段的表现和训练时有很大区别。主要注意三点输入图像要先letterbox变形保持宽高比不变否则检测框坐标会偏分割头的原始输出是float概率图要经过sigmoid和阈值化才转成mask检测框和分割mask后处理完要映射回原图分辨率。推理脚本核心部分import cv2 import torch def inference(model, img): letter_img, ratio, dwdh letterbox(img, (640, 640)) tensor torch.from_numpy(letter_img).permute(2, 0, 1).float() / 255.0 tensor tensor.unsqueeze(0).cuda() det_out, lane_out, drivable_out model(tensor) boxes postprocess_det(det_out, conf_thres0.4, iou_thres0.5) lane_mask torch.sigmoid(lane_out[0]) 0.45 drivable_mask torch.sigmoid(drivable_out[0]) 0.35 return scale_boxes(boxes, ratio, dwdh), lane_mask, drivable_mask掩码阈值值得单独说车道线的阈值设得比可行驶区域高是因为车道线本身是强结构目标低阈值会把大量噪声像素纳入进来导致车道线看起来像断断续续的毛刺。可行驶区域是区域目标对噪声不那么敏感低阈值可以保留更多边缘细节。5.2 可视化叠加的技巧道路识别结果的可视化很有观赏性直接把检测框和半透明mask叠加到原图。检测框颜色惯例是绿色框行人、蓝色框车辆、红色框交通标志。车道线我用纯白色或者黄色可行驶区域用半透明蓝色覆盖alpha值取0.3太大会遮挡人眼判断太小又看不清区域边界。可视化这个环节不只是为了好看它还是调试多任务模型最重要的工具。我在项目过程中反复把预测结果每帧叠加输出成视频一眼就能看出检测ok但车道线在路口断开、或者可行驶区域把对向车道也吃进去这类问题。如果只看指标这些问题很难量化定位。5.3 性能数据参考最后给一个实测数据参考。用YOLOv8s作Backbone的多任务模型参数量约18M比单独部署YOLOv8s加分割模型少了接近一半。在NVIDIA RTX 3080上batch size为1推理耗时约9msJetson Orin NX上用TensorRT FP16推理实测大约20ms一帧基本可以满足20到30帧每秒的实时需求。如果换成CPU部署一张640x640的图大约要跑600ms以上实时性就不要指望了只能做离线分析。因此嵌入式平台建议用YOLOv8n或者直接把分割头上采样层改成轻量反卷积速度和精度能找到一个相对平衡的落点。6. 我在实际训练中踩过的坑没踩过坑的多任务训练基本是不存在的。我自己的日志里至少记录了四类问题写出来帮大家提前绕开。第一是分割头上采样导致的显存爆炸。一开始我图省事直接用双线性插值上采样到640x640显存直接吃掉好几个G。后来把上采样分成两个阶段先上采样到160x160再上到640x640中间接一个轻量卷积显存占用下降超过40%精度基本不损失。第二是车道线的正负样本极端不均衡。前面提了Dice loss是解药但要注意Dice loss在正样本完全为空的图像上会出NaN所以训练时要去掉完全没有车道线的样本或者对这类样本跳过车道线loss计算。第三是mosaic增强对分割头不友好。Mosaic拼接把四张图硬拼在一起检测任务还能接受分割任务在拼接边界会产生错误的mask纹理。最后我改成前250个epoch用mosaic后面100个epoch关闭只做轻度仿射变换分割精度反而涨了大约两个点。第四是多任务训练初期的任务打架。第一个epoch结束检测loss降了分割loss纹丝不动。我把学习率调低一个数量级重新跑两个任务才开始同步收敛。说到底多任务训练的本质是一场平衡术检测和分割谁都不能太强势。整个项目做完我最大的体会是多任务模型调试时先把每个任务头单独训练到合理基线再加权合并会顺利很多。互相制衡的loss只会让模型什么都学不好。如果你正准备上手YOLO多任务道路识别我的建议就是先跑通一个最小demo用3万张图、短epoch数验证整个链路再考虑追求指标。这个项目让我对特征复用和任务权重分配有了完全不同的理解如果你也正在这条路线上调试希望这份项目代码整理能帮你少走几步弯路。本文还有配套的精品资源点击获取
返回列表