简介:这份PDF文档面向计算机视觉方向的学习者与工程开发者,聚焦多任务学习框架下YOLOv11同时实现目标检测与实例分割的完整工程实践,适合具备一定深度学习基础、希望掌握单阶段检测与像素级分割融合方案的中高级读者。文档共39页,以1个PDF文件形式打包,压缩包约2.19MB,支持目录章节跳转、阅读器左侧大纲显示与章节快速定位,查阅体验完整流畅。内容从多任务学习与目标检测、实例分割基础讲起,梳理YOLO系列发展脉络与YOLOv11骨干、颈部、头部架构设计,重点展开特征共享机制、检测分支与分割分支的融合设计及多任务损失权衡,并给出环境搭建、数据准备、模型训练、评估与部署的工程步骤,配合各模块代码实现解析与COCO及自定义数据集实验结果分析,还整理了常见问题与解决方案。目前已有99人学习,可帮助读者系统理解多任务融合思路、对照代码复现流程并积累排错经验。
1. 多任务学习框架下 YOLOv11 的检测与分割同屏输出:这条路值不值得走
如果你正在做工业质检、自动驾驶感知或者遥感解译,大概率遇到过这种局面:目标检测模型跑一遍拿到框,实例分割模型再跑一遍拿到掩码,两个模型各自占一份显存、各自吃一份推理时间,部署到 Jetson 或者边缘盒子上直接爆内存。多任务学习框架的核心思路就是让一个骨干网络同时输出检测框和实例掩码,YOLOv11 的seg分支恰好提供了这个能力——它不是在检测头上硬挂一个分割头,而是在训练阶段就让检测损失和分割损失联合回传,共享 backbone 和 neck 的特征。这意味着你只需要维护一套权重、一次前向推理,就能同时拿到boxes和masks。适合谁?适合已经跑通 YOLOv11 检测、想用最小改造成本把实例分割也纳入同一条流水线的工程师;也适合刚接触多任务学习、想找一个能落地、不玄学的切入点的新手。这一章不展开代码,先把「为什么是 YOLOv11 而不是自己拼两个模型」这件事说清楚。
YOLOv11 的seg模型本质上是一个多任务学习框架的工程化实现:检测头负责回归边界框和分类,分割头在检测框的基础上预测原型掩码系数,两者共享 C3k2 和 SPPF 提取的特征。这种设计的好处是推理时只跑一次 backbone,检测和分割的延迟几乎叠加在一个量级上,而不是两倍。代价是训练时两个任务的梯度会互相拉扯,小目标的分割精度往往不如单独训练的专用分割模型。所以选型时要问自己:你的场景是「检测为主、分割辅助」还是「分割精度优先」?前者直接上yolo11n-seg.pt微调,后者可能需要考虑 Mask R-CNN 或者自己设计解耦头。我一般会建议先用yolo11s-seg跑一个 baseline,看掩码的 mAP50-95 能不能接受,再决定要不要加深分割分支。
2. 从标注到训练:YOLOv11 多任务数据管线的搭建
2.1 检测框和分割掩码的标注格式怎么统一
YOLOv11 的seg模型训练时,标签文件里每一行同时包含检测框和分割多边形。格式是:class_id x1 y1 x2 y2 ... xn yn,其中多边形点按顺时针或逆时针排列,归一化到 0-1 之间。检测框不需要单独写,YOLOv11 在训练时会自动从多边形计算外接矩形作为检测目标。这意味着你不需要维护两份标注文件,一份多边形标注就够了。
常见做法是用 LabelMe 或者 CVAT 标注多边形,然后转成 YOLO 格式。LabelMe 输出的是 JSON,每个 shape 里有points数组。转换脚本的核心逻辑是:读取 JSON,对每个 shape 提取label和points,归一化坐标,写入 txt。下面是一个可以直接用的转换脚本:
import json import os from pathlib import Path def labelme_to_yolo_seg(json_path, output_dir, class_map): """ 将 LabelMe JSON 转为 YOLOv11 seg 格式的 txt class_map: {'person': 0, 'car': 1, ...} """ with open(json_path, 'r', encoding='utf-8') as f: data = json.load(f) img_w = data['imageWidth'] img_h = data['imageHeight'] lines = [] for shape in data['shapes']: label = shape['label'] if label not in class_map: continue cls_id = class_map[label] points = shape['points'] # 归一化并展平 norm_points = [] for x, y in points: norm_points.append(str(round(x / img_w, 6))) norm_points.append(str(round(y / img_h, 6))) lines.append(f"{cls_id} " + " ".join(norm_points)) out_path = Path(output_dir) / (Path(json_path).stem + '.txt') with open(out_path, 'w', encoding='utf-8') as f: f.write("\n".join(lines)) # 使用示例 class_map = {'defect': 0, 'scratch': 1} for jf in Path('labels_json').glob('*.json'): labelme_to_yolo_seg(str(jf), 'labels_txt', class_map)逻辑说明:class_map必须和你的data.yaml里的names顺序一致,否则训练时类别会错位。round(x / img_w, 6)保留六位小数,YOLOv11 官方推荐至少四位,六位更稳。如果多边形点数少于 3 个,LabelMe 里可能是矩形标注,这种要单独处理成四个点,否则 YOLOv11 会报polygon must have at least 3 points。
参数说明:output_dir要和图像目录同级,YOLOv11 默认在images同级的labels目录找标签。如果你的目录结构是dataset/images/train/和dataset/labels/train/,那output_dir就指向dataset/labels/train/。
2.2 data.yaml 里 seg 任务的关键字段
YOLOv11 的data.yaml和检测任务基本一样,但有一个隐藏坑:task字段必须显式写成segment,否则 Ultralytics 会按检测任务加载,分割头不会被初始化。下面是一个最小可用的配置:
# dataset/data.yaml path: /home/user/dataset train: images/train val: images/val test: images/test task: segment names: 0: defect 1: scratch 2: stainpath是数据集根目录,train和val是相对路径。task: segment这个字段在官方文档里提得不多,但如果你用yolo train命令时没有指定task=segment,模型会默认加载检测权重,分割分支的 loss 不会回传。我一般会在训练命令里也显式带上task=segment,双保险。
2.3 训练命令与多任务损失权重
YOLOv11 的seg训练命令和检测几乎一样,只是模型换成-seg后缀:
yolo segment train \ model=yolo11s-seg.pt \ data=dataset/data.yaml \ epochs=150 \ imgsz=640 \ batch=16 \ device=0 \ task=segment \ project=runs/seg_train \ name=exp01 \ patience=30 \ lr0=0.01 \ lrf=0.01 \ mosaic=1.0 \ copy_paste=0.3关键参数说明:copy_paste=0.3是分割任务特有的增强,它把一张图里的实例复制粘贴到另一张图,对分割掩码的边界学习很有帮助,但值太高会导致过拟合,0.1 到 0.3 之间比较稳。mosaic=1.0是默认值,如果小目标多可以保持,如果大目标为主可以降到 0.5。lr0=0.01是初始学习率,lrf=0.01是最终学习率因子,YOLOv11 默认用余弦退火,这两个值一般不用大改。
训练时看什么?看train/seg_loss和train/box_loss的下降曲线。如果seg_loss震荡厉害而box_loss平稳,说明分割头的梯度被检测头压制了,可以尝试调低box的损失权重——但 YOLOv11 没有直接暴露这个超参,常见做法是冻结 backbone 前几层先训分割头几个 epoch,再解冻联合训练。这个技巧在工业缺陷分割里很管用,因为缺陷的边界往往比整体轮廓更重要。
3. 推理与后处理:一次前向拿到框和掩码
3.1 Python 推理接口与结果解析
训练完之后,推理接口和检测任务几乎一样,只是返回的Results对象里多了masks字段:
from ultralytics import YOLO import cv2 import numpy as np model = YOLO('runs/seg_train/exp01/weights/best.pt') results = model.predict( source='test_images/', imgsz=640, conf=0.25, iou=0.45, retina_masks=True, # 关键:返回原图分辨率掩码 save=True, project='runs/seg_infer', name='test01' ) for r in results: boxes = r.boxes.xyxy.cpu().numpy() # N x 4 masks = r.masks.data.cpu().numpy() # N x H x W classes = r.boxes.cls.cpu().numpy() # N confs = r.boxes.conf.cpu().numpy() # N # 把掩码叠加到原图 img = r.orig_img for i, mask in enumerate(masks): mask_resized = cv2.resize(mask, (img.shape[1], img.shape[0])) color = np.random.randint(0, 255, 3).tolist() img[mask_resized > 0.5] = img[mask_resized > 0.5] * 0.5 + np.array(color) * 0.5 cv2.imwrite(f'overlay_{r.path.split("/")[-1]}', img)逻辑说明:retina_masks=True是关键参数。默认情况下 YOLOv11 返回的掩码是 160x160 的原型掩码,需要上采样到原图尺寸。开启retina_masks后直接返回原图分辨率的二值掩码,省去手动 resize 的麻烦,但显存占用会高一些。如果部署在边缘设备上,建议关掉retina_masks,在 CPU 上用cv2.resize做上采样,反而更省显存。
参数说明:conf=0.25和iou=0.45是通用起点。分割任务里iou对掩码的 NMS 影响不大,因为掩码 NMS 用的是框的 IoU,但conf太低会导致大量碎片掩码,后处理时可以用面积过滤:mask.sum() < 100的掩码直接丢掉。
3.2 掩码后处理的三个实用技巧
第一个技巧是掩码面积过滤。YOLOv11 的seg模型在小目标上容易产生噪点掩码,面积小于 50 像素的基本可以认为是误检。第二个技巧是掩码与框的对齐检查:如果掩码的质心偏离检测框中心超过框宽度的 30%,这个掩码大概率是错的,可以丢弃。第三个技巧是多边形简化:如果下游需要矢量边界,用cv2.findContours加cv2.approxPolyDP把掩码转成多边形,epsilon设为轮廓周长的 0.002 到 0.005 之间,既能压缩点数又不丢形状。
import cv2 import numpy as np def mask_to_polygon(mask, epsilon_ratio=0.003): """将二值掩码转为简化多边形""" mask_u8 = (mask * 255).astype(np.uint8) contours, _ = cv2.findContours(mask_u8, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) polygons = [] for cnt in contours: if cv2.contourArea(cnt) < 50: continue epsilon = epsilon_ratio * cv2.arcLength(cnt, True) approx = cv2.approxPolyDP(cnt, epsilon, True) polygons.append(approx.reshape(-1, 2)) return polygons这个函数返回的多边形点可以直接喂给标注工具做复核,或者存成 JSON 给下游的测量模块用。epsilon_ratio越大,多边形越简单,但边界越粗糙。工业测量场景建议 0.001 到 0.002,可视化场景 0.005 就够。
3.3 导出 ONNX 和 TensorRT 时的分割头处理
YOLOv11 的seg模型导出 ONNX 时,输出节点比检测模型多一个output1,形状是[1, 32, 160, 160],这是原型掩码。TensorRT 部署时要注意:如果只用检测结果,可以只解析output0;如果要分割掩码,需要把output1和检测框的掩码系数做矩阵乘法,再上采样。Ultralytics 的export命令会自动处理这些:
yolo export model=best.pt format=engine half=True device=0 imgsz=640导出 TensorRT 引擎时,half=True开启 FP16,分割掩码的精度损失通常在 1% 以内,但速度提升明显。如果发现掩码边缘出现锯齿,可以关掉half用 FP32 对比一下,确认是量化误差还是模型本身的问题。
4. 避坑与排查:多任务训练里最容易翻车的五个地方
4.1 现象:训练 loss 正常下降但掩码全是背景
原因:data.yaml里task字段没写或者写成了detect,YOLOv11 按检测任务加载,分割头虽然存在但 loss 权重为 0,梯度不回传。解决:检查data.yaml和训练命令里是否都有task=segment,缺一不可。
4.2 现象:推理时r.masks为 None
原因:模型加载的是检测权重而不是分割权重。YOLO('yolo11s.pt')加载的是检测模型,YOLO('yolo11s-seg.pt')才是分割模型。解决:确认模型文件名带-seg后缀,或者训练时用了task=segment。
4.3 现象:小目标的分割掩码断裂成几块
原因:YOLOv11 的原型掩码分辨率是 160x160,小目标在原型掩码上只占几个像素,上采样后边界不连续。解决:训练时把imgsz提到 1024 或 1280,或者用retina_masks=True推理,但后者不解决训练时的分辨率瓶颈。更彻底的做法是修改分割头上采样倍数,但这需要改模型结构,不建议新手动。
4.4 现象:copy_paste增强导致掩码和框不对齐
原因:copy_paste在复制实例时,如果两张图的尺度差异大,粘贴后的掩码坐标没有同步缩放。解决:把copy_paste降到 0.1 以下,或者改用copy_paste_mode='flip',这个模式只做翻转粘贴,尺度一致性更好。
4.5 现象:TensorRT 推理时掩码全黑
原因:导出 ONNX 时没有指定task=segment,或者 TensorRT 解析时只取了output0。解决:导出命令加task=segment,TensorRT 推理脚本里同时解析output0和output1,按官方后处理逻辑做矩阵乘法。
5. 把多任务框架压到 30 FPS:一个 Jetson 上的调优习惯
在 Jetson Orin Nano 上部署yolo11s-seg,默认 FP32 推理大概 12 到 15 FPS,离 30 FPS 还有距离。我一般会按这个顺序调:先导出 TensorRT FP16 引擎,FPS 能到 22 左右;再把imgsz从 640 降到 512,FPS 到 28 但小目标掩码质量下降明显;最后把retina_masks关掉,掩码上采样放到 CPU 上用cv2.resize做,FPS 稳定在 31 到 33 之间,CPU 占用增加不到 10%。这个组合在工业质检的传送带场景里够用了,掩码边缘的轻微锯齿不影响面积测量。
验证方法很简单:用trtexec跑 benchmark 看纯推理延迟,再用实际视频流跑端到端延迟。如果trtexec显示 25ms 但端到端 40ms,瓶颈大概率在前后处理,这时候把预处理从 Python 换成 C++ 或者 CUDA kernel,能再挤出 5 到 8ms。我习惯在调优前先跑一遍nsys profile,看清楚时间花在哪,再决定动哪里,不然容易白忙活。
最后说一个血泪教训:多任务模型的权重不要和检测模型混用。我有一次直接把yolo11s.pt的权重加载到seg模型里做初始化,结果分割头随机初始化,训练了 50 个 epoch 掩码 mAP 还是 0.1。后来换成yolo11s-seg.pt预训练权重,10 个 epoch 就到 0.45 了。预训练权重的选择比超参调优重要得多,这个后悔药我吃过一次就够了。希望帮到你。
本文还有配套的精品资源,点击获取