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

资讯详情

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

多任务YOLOv8实战:检测、可行驶区域与车道线分割一网打尽

多任务YOLOv8实战:检测、可行驶区域与车道线分割一网打尽

简介:面向自动驾驶实时感知场景,这套解决方案将YOLOv8目标检测、可行驶区域分割与车道线分割集成于一个轻量级多任务模型中。模型采用统一设计的轻量级分割头和统一损失函数,无需针对特定子任务定制模块,仅由卷积层堆叠构成,兼顾精度与推理效率,特别适合实时性要求高的车载或边缘平台。包内共1313个文件,以Python源码、Markdown文档、YAML配置和模型权重为主,另有少量可执行文件、环境脚本、Dockerfile、虚拟环境依赖等辅助素材,覆盖从模型定义、环境配置到部署依赖的完整链路。压缩包整体约28.54MB,目录规模适中,便于按需检索。目前已有1348人学习,适合自动驾驶、图像分割方向的研究生、算法工程师及竞赛学习者,可作为多任务感知模型设计、轻量分割头开发以及统一损失函数调优的参考范例。

1. 多任务 YOLOv8 不是把三个模型塞进一个网:先搞清楚要解决什么问题

在车载感知、机器人导航或者园区安防里,同一个摄像头画面往往要同时回答很多问题:哪里有车哪有人、哪些物理区域可以开过去、车道线在哪。如果按最省事的做法,检测跑一个 YOLOv8,分割跑一个 YOLOv8,两路模型同时部署,一帧图像两次前向推理,显卡和功耗立刻告急。把目标检测、可行驶区域分割、车道线分割压进同一个 YOLOv8 结构里,共享主干网络和特征层,一次 forward 同时输出检测框、区域掩码和车道线掩码,这样算力账才吃得下。这篇文章写给打算用 YOLOv8 做多任务感知的人:新手能照着准备数据、调通训练,熟手也能绕开我当年在损失函数和后处理上踩过的几个坑。后面先讲结构选型,再给数据集方案和可复现的训练命令,最后一节放一个能直接抄的推理校验技巧。

2. 任务组合与网络结构:为什么检测头和分割头能共享一个骨架

2.1 共享骨架与多分支 Head:一张 YOLOv8 网络结构图背后的算账逻辑

YOLOv8 网络结构图可以从下往上拆成三块:backbone 用 C2f 模块提取基础特征,neck 用 PAFPN 把不同尺度的特征做融合,最后接头部输出。多任务版本没有改变前两块的架构,只在 neck 后面多挂几个分支头。常见做法是保留一个检测头,再并列加两个分割头,三个头共享同一组特征金字塔输出。这样做最大的好处是参数增量很小,检测头本身只占整个模型尾部一小部分,分割头也只是一两个卷积层加一个上采样层。以 YOLOv8s 这个量级为例,单独检测模型大约 11M 参数;加一个分割头一般增加 1.5M 到 2.5M 参数,两个分割头加起来也不会让模型体积翻倍。相比各自独立模型,多任务模型参数总量大概能省三分之一到二分之一,推理延迟省得更多,因为只有一次主干计算。

但共享特征不是没有代价。检测任务希望特征有平移不变性和较高的感受野,用来框出完整物体;车道线分割任务希望特征保留精细的边缘信息和连续走向;可行驶区域分割则更像大块语义分类,对边缘精度要求略低。这三个任务放在一起,理论上存在负迁移:某些特征对检测有效但对车道线有害。实际经验是,只要数据量足够、损失权重调匀,正向迁移远大于负迁移,因为三个任务都建立在「场景整体理解」这个共同基座上。我在做多任务头时也遇到过分割头把所有电线杆都认成车道线的情况,这是因为共享特征把「长条状物体」这个模式过度放大了,后来靠把车道线分支的输入特征从多个尺度拼接、并加上上下文注意力,才把误检压下去。总的原则是:同一份特征对三个任务的适配程度不会完全一样,所以各分支头最好保留一点独立卷积去矫正特征。

2.2 可行驶区域和车道线为什么选分割头而不是检测头

有人会问,车道线能不能用目标检测的旋转框来标?理论上可以,但实际很别扭。车道线是细长曲线,一个旋转框会把大量背景包进来,回归目标不稳定,且弯曲车道线用矩形框表达本身就信息丢失。分割头输出的是像素级概率图,每一个像素回答「这是不是车道线」,天然适合曲线和遮挡场景。可行驶区域也一样,它本质上是二分类语义分割,边界往往延伸到图像边缘,用检测框会框进大量不可行驶的杂物,只有逐像素掩码能准确描述「能开到哪里」。

那为什么不干脆去掉检测头,只做两个分割头?因为目标检测的输出并不仅仅用于画框。框经过 NMS 之后能直接接跟踪、计数和测距,这些下游逻辑依赖一个紧凑的矩形描述。分割掩码要得到同类效果,还得额外做连通域分析或轮廓拟合,稳定性反而不如检测头。多任务 YOLOv8 保留了检测头,正是为了把「目标是什么、在哪」和「哪里可走、线在哪」分离表达。在自动驾驶典型场景里,检测头负责车辆、行人、骑行者的位置和置信度,可行驶区域分割头负责可通行边界,车道线分割头负责结构化道路信息,三个输出互不冲突,最后可以在后处理里互相校验。

2.3 一个模型还是三个模型:参数账、显存账和工程账

很多人在项目评估阶段会纠结,与其把 YOLOv8 改成多任务,不如分别部署三个模型,每个模型单独调到最优。这个思路没有错,但要看部署条件。先算显存:三个独立模型在推理时,即便可以分时复用同一块显存,峰值也要放下不止一个模型的工作集,因为中间特征图不一定能及时释放。在 Jetson Orin、RK3588 这类嵌入式板子上,显存和带宽都紧,多任务单模型明显更划算。再算延迟:三个模型串行推理,假设每个模型跑 20ms,总延迟约 60ms;多任务模型的主干计算只有一次,检测头和分割头并行分支,总延迟一般能压到 30ms 左右,终端体验差距明显。

不过工程上独立模型也有优点:可以分别打不同版本的补丁,某一任务数据集更新了不用重训全部模型;某个任务可以换成效果更好的候选模型,改动不牵连其他任务。多任务模型一旦训出来,就是一坨耦合的黑匣子,改一个损失函数可能导致所有任务指标波动。我的建议是:如果算力紧张、任务间共享语义明显,直接上多任务;如果你有团队长期迭代、不同任务由不同人维护,三个独立模型未必是坏事。对于文章场景,多任务模型在数据上有天然优势:它让同一个摄像头画面产生三类监督信号,等于一次标注喂给三个任务,这一点是独立模型做不到的。评估收益最直观的方法是,先把三个独立模型跑通,记录总参数量、显存峰值和帧率;再用多任务版本替换,同一份数据训练,看端到端帧率和 mIoU 是否在可接受的范围内。

方案参数总量推理方式峰值显存维护复杂度
三个独立 YOLOv8约 3 倍单模型串行 3 次前向高低,可单独替换
一个多任务 YOLOv8约 1.3~1.6 倍单模型单次前向低高,调参互相影响

这个表格里的倍数是我在多种任务组合下得到的经验范围,不是某个特定版本的精确实测。你可以拿自己的模型跑一下,把数字填进去作为方案会审依据。

3. 准备数据集与标注:一张图要同时标三类标签

3.1 标注格式:检测用 YOLO 框,分割用 PNG mask

YOLOv8 检测训练常见格式是每个 txt 文件里写若干行,每行五个数:类别编号、归一化中心 x、归一化中心 y、归一化宽、归一化高。这个格式成熟稳定,直接复用。但两个分割任务没有现成的 YOLO 格式,需要单独准备。可行驶区域分割和车道线分割本质上都是单通道语义图,我一般把它们保存成 PNG 掩码文件,背景像素值 0,目标像素值 1。注意是单通道灰度 PNG,不是三通道彩色图,否则读取时可能被 resize 出奇怪结果。

整个数据集目录我建议这样组织:

dataset/ images/ train/ img_001.jpg img_002.jpg val/ img_001.jpg labels/ train/ img_001.txt img_002.txt val/ img_001.txt seg_drive/ train/ img_001.png img_002.png val/ img_001.png seg_lane/ train/ img_001.png img_002.png val/ img_001.png

这个目录结构里,images 放原始图像,labels 放目标检测的 YOLO 格式标签,seg_drive 和 seg_lane 分别放两个分割任务的掩码。三个标注文件与图像文件名一一对应,要求同名且格式不同,训练脚本加载时按文件名匹配。图像和掩码的尺寸必须保持一致,不然训练时 tensor 对齐会出现维度错误。

这里注意一个容易忽略的点:目标检测标签中的类别和分割掩码中的类别是两套体系。检测框里的「车辆」「行人」类别编码有十几个,分割掩码里「可行驶区域」和「车道线」各自只有一类。千万不要把分割掩码里的类别编号直接放进检测标签,也不要反过来。它们只是在同一张图片上同时存在的两种监督信息,互不替代。

3.2 用 LabelMe 标可行驶区域和车道线:标注流程与导出脚本

目标检测常用标注工具很多,LabelMe 是其中比较适合同时画框和多边形的一种。它可以把检测框和分割多边形放在同一个 JSON 文件里,但 YOLO 和 PNG mask 需要分别导出,所以我的工作流是分两轮:第一轮先标检测框,导出 YOLO 格式 txt;第二轮再画可行驶区域和车道线的多边形,导出 PNG mask。两轮标注共用同一个图片目录,可以明显降低返工成本。

LabelMe 原始标注是 JSON,里面每一笔多边形是一条 shape。实际转换时不一定装完整图形界面,直接用 Python 写脚本就能把 JSON 转成 mask:

import json import numpy as np import cv2 from labelme import utils with open('img_001.json') as f: data = json.load(f) img = utils.img_b64_to_array(data['imageData']) label_name_to_value = {'_background_': 0, '_objects_': 1} mask, _ = utils.shapes_to_label( img.shape[:2], data['shapes'], label_name_to_value, ) mask_png = (mask > 0).astype(np.uint8) cv2.imwrite('seg_drive/img_001.png', mask_png * 255)

这段代码先把 LabelMe 的 JSON 读进来,用 img_b64_to_array 还原原始图像尺寸,再通过 shapes_to_label 把多边形填充成掩码。最后乘以 255 是因为掩码要写进 PNG 可视化,训练时读到再除以 255 转回 0/1。注意 shapes_to_label 的 label_name_to_value 映射,如果同一个 JSON 里同时存在「可行驶区域」和「车道线」两类,你需要分成两个不同映射分别导出,或者先按名称拆分 shapes 再导出。更稳妥的做法是可行驶区域和车道线分开标注成两个 JSON,避免一边画错影响另一边。

导出之后必须做一次校验:把 PNG mask 缩放到原始图大小,再用半透明色叠加到原图上,肉眼确认边界有没有错位。常见问题是 LabelMe 的坐标是整数像素,而图片 resize 后 mask 也跟着变,如果训练脚本对图像做了 letterbox,那么 mask 要做同样参数的 letterbox,否则对不齐。

3.3 数据增强的坑:翻转、裁剪会破坏空间语义

多任务训练里,数据增强是最容易让模型学会错误规律的环节。先看水平翻转。目标检测框水平翻转之后坐标同步变换,类别不变,没问题。分割掩码水平翻转也同步变,但这里有一个潜规则:如果车道线分割任务要区分左车道线和右车道线,那么翻转后左变右、右变左,标签必须交换左右类别。如果你只是单类「车道线」,那就没事,翻转只会让模型学到更对称的特征。可行驶区域同理,翻转后仍是可行驶区域。

随机裁剪和缩放也要谨慎。检测框裁剪后可能不完整,但 YOLO 训练本来就能适应部分遮挡;车道线被裁掉一段后,掩码会出现断裂,模型会被迫学习「车道线可以中间断开」的错误先验。所以我通常对分割任务不做随机裁剪,只做整图缩放和轻微旋转,旋转角度控制在正负 10 度以内,过大会让远处车道线扭曲到不合理的几何形状。色彩增强方面,亮度对比度调整会影响白色车道线和浅色路面的对比度,极端参数会把两者混成一片。建议把 hue 偏移量设为 0,saturation 只在 0.8 到 1.2 之间浮动。我自己曾经把 brightness 调到 0.6,结果可行驶区域的 mIoU 掉了三个点,当时还没意识到是颜色增强搞的鬼。

数据增强策略本质上是给模型加先验:越强的增强会让模型更鲁棒,但也会侵蚀结构化任务的几何约束。车道线和可行驶区域都是空间结构信息,增强时优先保证结构的连续性,其次再考虑多样性。

4. 搭建多任务训练环境:从 YOLOv8 源码到第一个模型跑通

4.1 在 Ubuntu 上搭 YOLOv8 CPU 环境:最小安装命令

很多人拿着 YOLOv8 代码第一件事就是配置环境,这里给一个在 Ubuntu 20.04 上能跑通的最小方案。先建一个独立的 conda 环境,避免把系统 Python 环境改乱。然后是安装 PyTorch 的 CPU 版本,这个可以直接从 CPU 索引装,不需要碰 GPU 驱动:

conda create -n yolomulti python=3.8 conda activate yolomulti pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install ultralytics opencv-python labelme

安装完成之后用python -c "import torch; print(torch.__version__)"验证。如果输出正常,说明基础环境没问题。这里重点说明一下为什么指定python=3.8:YOLOv8 系列在 3.8 到 3.10 上都跑得很稳,3.8 对很多算法库兼容性更好,避免后面装编译型依赖时报错。

CPU 环境下训练多任务模型确实辛苦,但并不是不能跑。关键是把输入分辨率调小、batch 调小、epoch 数减半。后续训练命令里我会给出一组能直接用的参数。如果你后续要到 GPU 或 RK3588 上部署,环境也不需要推倒重来,只需要重新安装对应平台版本的 PyTorch,模型代码保持统一。

4.2 自定义多任务头:修改配置还是重写损失

Ultralytics 官方 YOLOv8 没有直接提供一个「检测 + 两个分割头」的现成配置。常见做法是改源码:在模型定义文件里把 Detect 头替换成一个自定义的多任务头,并在训练脚本里同时计算三种损失。不要试图通过改 yaml 配置文件来凭空生成一个不存在的 head,那样只会得到一堆被忽略的未知字段。

下面是一个极简的多任务头定义,突出三个分支的挂载方式:

import torch.nn as nn class MultiTaskHead(nn.Module): def __init__(self, det_head, in_channels): super().__init__() self.det = det_head self.drive_head = nn.Sequential( nn.Conv2d(in_channels, 64, 3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.Conv2d(64, 1, 1), nn.Upsample(scale_factor=4, mode='bilinear', align_corners=False), ) self.lane_head = nn.Sequential( nn.Conv2d(in_channels, 64, 3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.Conv2d(64, 1, 1), nn.Upsample(scale_factor=4, mode='bilinear', align_corners=False), ) def forward(self, features, targets=None): det_out = self.det(features) drive_out = self.drive_head(features[-1]) lane_out = self.lane_head(features[-1]) return det_out, drive_out, lane_out

这段代码里,det_out 沿用原检测头的输出格式,drive_head 和 lane_head 分别从特征金字塔的最后一层接出来,通过一个卷积和上采样还原到原图 1/4 分辨率。in_channels 要取 neck 最后一层的输出通道数,例如 YOLOv8s 是 256。注意这里分割头只用了最高层的特征,精度可能不够,实际工程里可以把三层金字塔特征都上采样后 concat 再卷积,但那样代码会明显变长。

训练时,损失函数要拆开算:

def multi_task_loss(preds, det_targets, drive_masks, lane_masks, weight_det=1.0, weight_drive=0.5, weight_lane=0.5): det_out, drive_out, lane_out = preds loss_det = det_loss(det_out, det_targets) loss_drive = dice_loss(drive_out, drive_masks) loss_lane = dice_loss(lane_out, lane_masks) return weight_det * loss_det + weight_drive * loss_drive + weight_lane * loss_lane

建议先分别用 1.0、1.0、1.0 跑一个短 epoch,打印出三种损失的数值范围,然后再调权重。我在项目里的经验是分割损失普遍比检测损失小,如果不提高权重,分割头基本学不到东西。

4.3 训练命令与四个必调参数

一旦模型头改好,训练循环和官方流程差别不大。如果你沿用 ultralytics 的训练器,需要把损失函数替换成上面那个多任务版本。为了方便排错,我更喜欢直接用 PyTorch 写一个简洁的训练循环:

for epoch in range(epochs): model.train() for images, det_labels, drive_masks, lane_masks in dataloader: images = images.to(device) drive_masks = drive_masks.to(device) lane_masks = lane_masks.to(device) preds = model(images) loss = multi_task_loss( preds, det_labels, drive_masks, lane_masks, weight_det=1.0, weight_drive=0.8, weight_lane=0.8, ) optimizer.zero_grad() loss.backward() optimizer.step() torch.cuda.synchronize()

这一段里的权重是我调试后比较稳的一组,但不同数据集差异很大。如果你的检测框很多、分割目标很大,权重还要调整。训练时除了损失权重,第一个必调参数是输入分辨率。官方默认 640x640,CPU 上跑 640 会非常慢,我建议在模型文件里把输入降成 416x416,检测数量多的小目标场景再恢复到 640。第二个必调参数是 batch size。CPU 环境下 batch 设置 4 比较稳妥,显卡 16G 显存可以到 16。注意 batch 过小时,BatchNorm 统计量波动很大,分割头的输出会出现闪烁,这时可以把 BN 换成 GroupNorm。第三个必调参数是学习率。官方默认优化器是 SGD 加很大初始学习率,GPU 上没问题,CPU 环境收敛慢,我更推荐用 AdamW,初始学习率 0.001,配合CosineAnnealing。第四个必调参数是 epoch 数。多任务比单任务往往需要更多 epoch 才稳定,但受限于 CPU 算力,建议先设 60 个 epoch,每 5 个 epoch 保存一次权重,观察损失曲线是否单调下降。

这里牵扯到一个用户常搜的问题:yolov8 模型训练参数含义。如果你打算用官方训练器,--epochs、--batch、--imgsz这些参数名和这里是一致的,但多任务损失权重官方没有暴露接口,还是需要改源码。所以我的建议很直接:多任务项目别依赖官方命令,老老实实写一个自己的训练脚本,调试起来会舒服得多。

5. 多任务训练避坑指南:我在这类项目里踩过的 4 个坑

5.1 坑一:检测损失和分割损失数量级不对等,分割任务被带崩

现象:训练十几轮后,目标检测的 mAP 已经在稳步上涨,但可行驶区域的 mIoU 一直在 0.15 以下徘徊,而且每次验证时掩码区域都在缩小,最后只剩画面中央一块。

原因:检测损失由分类损失和回归损失组成,数值普遍比分割损失大一个到两个数量级。当两个损失直接相加时,反向传播梯度被检测损失主导,分割头虽然也在降损失,但更新的方向被反复撕扯,很难收敛到有效区域。这是多任务训练最经典的失衡问题。

解决:先在训练脚本里打印每个损失项,确认它们的平均值和量级。比如检测损失均值是 6.5,分割损失均值是 1.2,那么就把分割权重调到 3.0 左右。我习惯把权重初始化成loss_det / loss_drive的比例,再做一轮微调。另外可以把分割损失换成 Dice 损失,它的数值比较稳定,不会因为目标像素少而剧烈波动。我在换了 Dice 之后,mIoU 从 0.15 提到了 0.3,效果立竿见影。

5.2 坑二:可行驶区域 mask 没有边缘软化,分割结果锯齿严重

现象:训练收敛后,把分割概率图阈值化成 0/1,发现可行驶区域边缘有一圈毛刺,尤其在路面和路肩交界处,锯齿每隔几个像素就跳变。视觉上像低分辨率贴图放大后的效果。

原因:标注 mask 是硬标签,边界像素被强行分成 0 和 1,模型在边缘附近会输出很陡峭的置信度变化。语义分割里,这种硬边界对 BCE 损失不友好,因为模型无法正确估计边界的梯度方向,被迫在几个像素之间摇摆。

解决:训练前对 mask 做一次高斯模糊,把边缘从硬 0/1 变成 0 到 1 的连续过渡。常见做法是用cv2.GaussianBlur(mask, (5, 5), 3)对 GT mask 做平滑,这样模型学到的是带模糊带的概率图,推理时再取 0.5 阈值,边缘反而更稳定。注意模糊半径不要太大,否则车道线这种细线会被抹掉。车道线 mask 我一般只做3x3模糊,甚至不用模糊,因为车道线本身只有几个像素宽,过度平滑会直接把目标融进背景。可行驶区域可以做稍大尺寸的模糊,因为它面积大,边缘过渡不影响整体结构。

5.3 坑三:三个头的输出尺寸和通道数不一样,后处理写错索引

现象:推理时拿到模型输出,直接取pred[0]做 NMS,结果发现得到的是一个四维张量,里面的数值根本不是检测框,而是分割头的概率图。屏幕上一片花,或者直接报维度不匹配。

原因:多任务模型的 forward 返回是一个元组,检测头返回的是多个尺度特征级联后的列表,分割头返回的是单张掩码。不同源码实现的排列顺序不同,有人习惯把检测结果放第一位,我自己的项目里也遇到过把分割头放前面的时候。后处理代码写死了索引,一旦输出顺序变化就会踩坑。

解决:在拿到模型第一版权重后,先跑一次确定性推理,把三个返回值的 shape 和数值范围都打印出来。增长期维护时,给这个推理函数写一个简单的单元测试。

det_out, drive_out, lane_out = model(images) print(det_out.shape, drive_out.shape, lane_out.shape)

推理时定义一个输出打包函数,只允许按指定名称读取:

results = { 'det': det_out, 'drive': drive_out, 'lane': lane_out, }

这样后处理什么索引都不会写错。另外还要确认检测头的输出到底是一个 list 还是张量,YOLOv8 官方在训练时输出列表,在部署时导出模型又是另一个形式,千万别用同一个索引策略同时处理两种模式。很多模型转换到 RK3588 或 TensorRT 后输出顺序会乱,提前用字典约束住,后面会省很多事。

5.4 坑四:CPU 环境训练半天不收敛,问题不在模型而在学习率和 batch

现象:在 CPU 环境跑多任务模型,设了 200 个 epoch,跑了两个小时,损失曲线一直横着走,偶尔上下跳动,看不出下降趋势。检查模型定义没发现错,数据集也检查过没问题。

原因:CPU 训练时 batch 被迫设得很小,比如 2 到 4。小 batch 导致 BatchNorm 统计量不稳定,梯度方向噪声极大。同时很多教程直接沿用 GPU 训练的大学习率,比如 0.01 用于随机梯度下降。这两个因素叠加,模型就在原地打转。

解决:把 batch 尽量设到 8 以上,内存不够就降低输入分辨率到 416,甚至 320。然后把优化器换成 AdamW,学习率调到 0.001,并做一个 5 个 epoch 的 warmup,让学习率从 0.0001 慢慢爬升。另一个有效手段是把 BatchNorm 换成 GroupNorm,但需要改模型定义,成本更高。我的建议是优先优化学习率和 batch。这套组合在几个项目里都让损失在 20 个 epoch 内开始下降,被称为「后悔药」一点也不夸张。

6. 一个实用技巧:用多任务输出的互相校验提高结果可用性

6.1 把三个结果叠加到一张图上,快速判断任务间的空间一致性

多任务模型的调试难点在于三个输出之间的相对位置是否正确。我建议每次验证时都把检测框、可行驶区域和车道线用不同颜色叠到原图上,一眼就能看出问题。

import cv2 def visualize(images, det_out, drive_out, lane_out): drive_mask = (drive_out.squeeze(1).sigmoid() > 0.5).cpu().numpy() lane_mask = (lane_out.squeeze(1).sigmoid() > 0.5).cpu().numpy() drive_color = (0, 255, 0) lane_color = (0, 0, 255) for i in range(images.shape[0]): vis = images[i].permute(1, 2, 0).cpu().numpy().copy() vis = (vis * 255).astype('uint8') vis = cv2.addWeighted(vis, 0.7, drive_color, 0.3, 0) vis = cv2.addWeighted(vis, 0.7, lane_color, 0.3, 0) cv2.imwrite(f'vis_{i}.jpg', vis)

这段代码使用固定颜色给整张 mask 叠加染色,实际绘图时可以生成彩色掩码再叠加。检查的重点是车道线是否压在影像中的车道位置,检测框是否落在可行驶区域内部,三者有没有错位。如果检测框在可行驶区域外,说明两个头空间没有对齐,多半是预测时输入图像尺寸和 mask 上采样尺寸不一致,需要回去检查 letterbox 参数。

6.2 用可行驶区域掩码过滤检测框,去掉路外误检

一个常见误检是检测模型把路边的停车、树木错当成目标车辆。这类误检通常中心点落在可行驶区域外。既然多任务模型同时给了掩码,就可以直接用掩码值修正检测置信度,不需要另跑一个规则模型。

def filter_by_drive(det_boxes, det_confs, classes, drive_mask): filtered = [] for box, conf, cls in zip(det_boxes, det_confs, classes): x1, y1, x2, y2 = box cx = (x1 + x2) / 2 cy = (y1 + y2) / 2 h, w = drive_mask.shape[-2:] cx_pixel = int(cx * w) cy_pixel = int(cy * h) if drive_mask[0, cy_pixel, cx_pixel] > 0.5: filtered.append((box, conf, cls)) return filtered

这里假设drive_mask是模型输出经过 sigmoid 后的概率图,尺寸已经还原到原图大小。对每个检测框,只看框中心点是否落在可行驶区域内,不在就滤掉。注意这个技巧要慎用:行人可能站在路肩外,车辆正处在可行驶区域边缘,中心点落入 mask 外不代表这个目标完全不可用。更稳妥的方式是把中心点附近小邻域的概率均值作为置信度参考,而不是单个像素。

6.3 半精度推理与 TensorRT 导出的注意点

验证阶段如果觉得双精度推理太慢,可以先把模型转成半精度。半精度在 Ampere 架构 GPU 上有明显加速,在 CPU 上没有任何收益,反而可能因为不支持 FP16 报错。代码只需一行:

model.half() with torch.no_grad(): det_out, drive_out, lane_out = model(images.half().to(device))

导出 TensorRT 或 RKNN 时,三个输出的排列和名称也要提前固定。很多部署工具链只能识别固定数量输出,你需要把三个头的结果拼接成一个张量,或分别命名。我通常会把分割头输出插值到原图尺寸之后再导出,减少部署端预处理负担。这个方案在建图、避障、自动驾驶相关场景里都很常用,关键是先把多任务模型本身的输出一致性验证做好,再谈部署加速。我自己在 RK3588 上部署时,因为输出顺序没固定,整整排查了两天,最后靠打印输出 shape 才发现问题。这个教训让我养成了「先可视化、后加速」的习惯。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表