
简介基于深度学习方法的图像分割资源包完整覆盖语义分割、实例分割与全景分割三大任务适合计算机视觉相关专业的在校学生、算法工程师及毕设开发者参考实践。内容以Python代码为主结合Markdown说明文档与Qt工程文件展示了从单通道标签转换、遥感图像预处理到模型测试的典型流程能够帮助读者厘清不同分割任务的实现差异。压缩包共5个文件包含3个Markdown说明文档、1个Python脚本和1个项目工程文件整体仅11KB体积轻巧却结构清晰便于按需阅读。目前已有206人学习下载。该资源源自作者的毕业设计代码经过实际运行验证并整合了CCF遥感图像分割赛题的思路与素材附带的README提供了必要的使用指引运行中遇到问题也可向作者咨询。既可作为课程设计、项目初期的演示基底也适合在此基础上扩展其他功能是快速入门深度学习图像分割的高性价比资料。1. 图像分割项目怎么选型语义、实例、全景三条技术路线先分清Python 的深度学习项目里图像分割是比分类和检测更吃工程细节的一类任务。标题同时挂出语义分割、实例分割、全景分割说明这套源代码和设计资料覆盖的是完整的分割技术栈而不是单个模型的开箱演示。做广告牌图像分割系统、医学图像分割、遥感地物提取的人最后都会卡在同一个选择题上我到底该走哪条路线。我的答案很直接先花一周把三条路线的最小样例都跑通——语义分割做像素分类实例分割区分个体并给独立掩码全景分割把两者合并成统一的场景描述。下面按这个顺序从选型理由、数据集制作、训练参数一路讲到避坑记录适合正在写毕业设计或者要把分割模型接到实际业务里的工程师。2. 语义分割先跑通UNet 最小训练代码与数据集制作全流程2.1 为什么第一版选 UNet 而不是 DeepLabV3任务边界与硬件前提语义分割的本质是逐像素分类网络输出一张和输入同分辨率的类别索引图。这套项目里语义分割是最先落地的部分选型时我从来不看榜单而是先看手里的数据量和显卡。UNet 的编码器-解码器结构加跳跃连接对小数据集非常友好。医学图像分割社区长期以 UNet 系为主就是因为几千张图就能训练出可用的模型不需要大规模预训练。DeepLabV3 的 ASPP 空洞卷积模块擅长抓多尺度上下文但参数量和显存占用都上了一个台阶数据少于一万张时优势不明显。SegFormer 这类 Transformer 结构更是依赖大规模预训练自己从头训练很容易欠拟合训出来的效果大概率不如一个老老实实的 UNet。我一般这样定数据量在 1k 张以内、显存 8G第一版直接上 UNet输入裁剪到 256×256数据量上万、显存 24G再考虑 DeepLabV3 或 SegFormer。另外设计资料里如果画了系统需求分析和系统架构图通常也是按 UNet 这条线设计的——先跑通它后面换骨干网络只动编码器部分解码器和训练循环不用重写。启动之前把 Python 和 PyTorch 的深度学习环境配置好就行VSCode 或 PyCharm 都能跑不必在 IDE 上花时间纠结。2.2 语义分割数据集制作从标注文件到 DataLoader 的四个关键字段数据集制作是新手翻车最多的地方。用 LabelMe 或者任何标注工具画完多边形导出的是 JSON 或 VOC 格式的彩色 PNG但训练时要求的是单通道索引图不是三通道可视化图。0 号索引留给背景类别从 1 开始排255 表示该像素不参与损失计算。如果直接把 RGB 可视化图喂给 CrossEntropyLoss类别数和通道数对不上loss 会一直高位震荡怎么调学习率都没用。我习惯把数据集整理成四个关键字段img_dir 放原图mask_dir 放索引图class_num 写类别总数ignore_index 固定 255。下面是最小可用的 Dataset 类随机裁剪时保证原图和掩码用同一个偏移量。这个细节漏掉的话训练出来就是一张错位叠加的预测图而且损失曲线看起来完全正常。# seg_dataset.py语义分割 Dataset 最小实现 import os import numpy as np from PIL import Image import torch from torch.utils.data import Dataset from torchvision import transforms class SegDataset(Dataset): def __init__(self, img_dir, mask_dir, crop_size256): self.names sorted(os.listdir(img_dir)) self.img_dir, self.mask_dir img_dir, mask_dir self.crop_size crop_size self.norm transforms.Normalize( mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) # ImageNet 归一化 def __len__(self): return len(self.names) def __getitem__(self, idx): name self.names[idx] img Image.open(os.path.join(self.img_dir, name)).convert(RGB) mask_name name.rsplit(., 1)[0] .png mask Image.open(os.path.join(self.mask_dir, mask_name)).convert(L) w, h img.size cs min(self.crop_size, w, h) x torch.randint(0, w - cs 1, (1,)).item() y torch.randint(0, h - cs 1, (1,)).item() img img.crop((x, y, x cs, y cs)) mask mask.crop((x, y, x cs, y cs)) img self.norm(transforms.ToTensor()(img)) mask torch.as_tensor(np.array(mask), dtypetorch.long) return img, mask这个类里最值得注意的两个点一是裁剪偏移必须共享原图和标签各自随机裁剪的话数据对不上位置二是 mask 转成 long 型张量CrossEntropyLoss 要求 target 是长整型索引而不是浮点概率。归一化直接用 ImageNet 的均值和标准差这是从分类预训练迁移过来的惯例换成自己统计的均值反而容易让训练初期 loss 跳动。语义分割数据集制作到这里就完整了剩下就是按需做增强翻转和随机亮度扰动一般就够。2.3 UNet 训练脚本损失函数、学习率与 100 个 epoch 内的调参基准语义分割的损失函数我默认用 Dice 加交叉熵的混合。单独用交叉熵背景像素太多前景梯度被稀释单独用 Dice小目标区域梯度不稳定loss 曲线像过山车。两者相加是工程上最稳的组合目标就是让大背景和稀疏前景都能被照顾到。学习率先定 1e-4 的 AdamW这个值在 UNet 上几乎不会翻车往上加到 3e-4 就开始有训练不稳定的风险。# unet_train.py二分类语义分割最小训练循环 import torch import torch.nn as nn from torch.utils.data import DataLoader from models.unet import UNet # 编码器-解码器结构见项目 models/unet.py model UNet(in_channels3, out_channels2).cuda() optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) def dice_loss(logits, targets, smooth1.0): probs torch.softmax(logits, dim1)[:, 1] # 取前景通道 inter (probs * targets.float()).sum() union probs.sum() targets.float().sum() return 1.0 - (2.0 * inter smooth) / (union smooth) for epoch in range(100): model.train() for imgs, masks in train_loader: # masks: (B,H,W) 长整型索引图 imgs, masks imgs.cuda(), masks.cuda() logits model(imgs) # (B,2,H,W) loss dice_loss(logits, masks) nn.functional.cross_entropy( logits, masks, ignore_index255) optimizer.zero_grad() loss.backward() optimizer.step() if (epoch 1) % 20 0: torch.save(model.state_dict(), fcheckpoints/unet_{epoch1:03d}.pth)我的调参基准batch 在 8G 显存上取 8crop 256100 个 epoch 内二分类 mIoU 应该能到 0.8 以上。如果 30 个 epoch loss 纹丝不动先查归一化和类别索引不要急着换模型。项目里的设计资料一般会把模块拆成 model、dataset、train、infer 四个文件训练脚本和模型文件分离后面换 DeepLabV3 只改一行 import其余不动。这个习惯让我在反复试骨干网络的时候少改了很多无用代码。3. 实例分割换 Mask R-CNNanchor、ROI 与训练参数怎么定3.1 语义和实例差在一个「区分个体」任务定义与框架选型语义分割回答「这个像素属于哪一类」实例分割还要回答「这个像素属于哪一个物体」。同一个类别里出现五辆车语义分割只给一片合并的车掩码实例分割要给出五个独立掩码。任务定义变了网络结构就从纯卷积分割头变成检测加分割的两阶段结构因为「区分个体」本质上要先定位每个目标再在定位框内部细化掩码。可选框架里Mask R-CNN 是文档最全、最稳的选择torchvision 和 detectron2 都有官方实现设计资料里的系统架构图也大多是它。YOLO 系的实例分割走的是单阶段路线包括 yolo 最新版本里把 instance segmentation 和语义分割分开的分支设计推理快但掩码边缘通常比两阶段粗一些。我的判断标准要精度、要交毕业设计文档选 Mask R-CNN要实时性、要部署到边缘设备再研究 YOLO 实例分割。广告牌图像分割这类需要干净边缘的场景我默认 Mask R-CNN。3.2 Mask R-CNN 初始化与训练参数预训练权重、batch 与 anchor 调整实例分割从头训练基本不现实COCO 预训练权重是标配。torchvision 的 maskrcnn_resnet50_fpn 加载预训练后只需要替换最后的分类头和掩码头让类别数匹配「背景 你的目标类别数」。训练数据格式也要同步改每个样本的 target 要包含 boxes、labels、masks 三个字段其中 masks 是 (N,H,W) 的布尔矩阵N 是这张图里的目标个数。# mask_rcnn_train.py替换预测头并调整 anchor import torchvision from torchvision.models.detection import maskrcnn_resnet50_fpn from torchvision.models.detection.faster_rcnn import FastRCNNPredictor from torchvision.models.detection.mask_rcnn import MaskRCNNPredictor from torchvision.models.detection.anchor_utils import AnchorGenerator model maskrcnn_resnet50_fpn(weightsCOCO_V1) # 预训练权重 num_classes 2 # 背景 1 类目标 box_head model.roi_heads.box_predictor model.roi_heads.box_predictor FastRCNNPredictor( box_head.cls_score.in_features, num_classes) mask_head model.roi_heads.mask_predictor model.roi_heads.mask_predictor MaskRCNNPredictor( mask_head.conv5_mask.in_channels, 256, num_classes) # 小目标为主的数据集anchor 尺寸整体下探一档 model.roi_heads.anchor_generator AnchorGenerator( sizes((16, 32, 64, 128, 256),), aspect_ratios((0.5, 1.0, 2.0),))anchor 是最容易被忽略的参数。COCO 默认锚框从 32 到 512如果你检测的是广告牌、零件这类中小目标默认配置会漏检。调小到 16 到 256 之后小目标召回率通常能涨几个点。但注意 anchor 的组合数不要变sizes 和 aspect_ratios 的组合数量变了RPN 头部的输出通道也要同步改很多人在这里改崩溃过。batch 在实例分割里尤其吃显存8G 显卡只能跑 batch 2这时候不要硬上大 batch用梯度累积等效 batch 8比调小输入分辨率更稳。3.3 实例分割推理端到端掩码输出、NMS 与可视化保存推理时 Mask R-CNN 的输出是一个 dict包含 boxes、scores、labels、masks。masks 是 28×28 的 ROI 结果要先插值回原图尺寸再阈值化直接当原图掩码用会得到一堆锯齿。NMS 在 torchvision 里是现成的但实例分割的 NMS 有讲究——先做类别内的框抑制不同类的重叠目标不要互相抑制否则一辆车旁边站个人其中一个会被误删。# mask_rcnn_infer.py单图推理与掩码后处理 import torch import torchvision import torch.nn as nn model.eval() with torch.no_grad(): pred model([img_tensor])[0] # 输入是 list输出是 dict keep torchvision.ops.nms(pred[boxes], pred[scores], iou_threshold0.5) for idx in keep: if pred[scores][idx] 0.5: # 置信度过滤阈值按场景调 continue mask pred[masks][idx, 0] 0.5 # 28x28 ROI 掩码 mask nn.functional.interpolate( mask.unsqueeze(0).unsqueeze(0).float(), size(img_h, img_w), modebilinear)[0, 0] 0.5 # mask 现在是原图分辨率的布尔矩阵可以直接叠加到可视化图上这里我踩过一个坑如果不做 NMS 直接按分数排序取前 N 个两个高度重叠的框会输出几乎相同的掩码可视化时像目标「重影」。iou_threshold 取 0.5 是通用值目标密集的场景可以下调到 0.3让重叠目标尽量都保留下来。掩码阈值 0.5 也是经验值想保留更多边缘细节可以降到 0.3代价是背景噪声多一点。4. 全景分割把两类任务合流thing/stuff 标签体系与冲突消解4.1 全景分割在解决什么问题为什么不能直接把两个模型拼一起全景分割的动机很直接真实场景里既有「人、车、广告牌」这类可数的物体术语叫 thing也有「天空、道路、草地」这类不可数的背景术语叫 stuff。语义分割能覆盖两者但分不出个体实例分割只管 thing 不管 stuff。全景分割要求一个输出同时满足两个约束每个像素有且仅有一个语义标签thing 类别还要带实例编号。表面上看把语义分割和实例分割两个模型的结果拼起来就行。实际拼过的人都知道会出两类问题一是同一个区域被重复标记语义图里把一辆车标成「车」实例图又给它一个掩码融合时不知道听谁的二是 stuff 区域没有实例编号语义图和实例图在背景上的输出形式完全对不上。所以全景分割框架在训练阶段就把两个分支统一起来Panoptic FPN 这类结构共享骨干网络语义头负责 stuff 和粗糙的 thing 区域实例头负责精细的 thing 掩码最后用融合模块输出一张统一的全景标签图。4.2 用 detectron2 搭全景分割配置、数据与推理样例自己从零实现全景分割的工程成本很高我直接用 detectron2 搭。它的配置系统、数据加载、模型和评估都封装好了预训练权重从 model zoo 拿跑通推理只需要十几行。设计资料里如果要求写系统设计detectron2 的三层结构——骨干网络、语义头、实例头——正好可以原样画进架构图里答辩或者评审时一看就懂。# panoptic_infer.pydetectron2 全景分割推理 from detectron2.config import get_cfg from detectron2.engine import DefaultPredictor from detectron2 import model_zoo cfg get_cfg() cfg.merge_from_file(model_zoo.get_config_file( COCO-PanopticSegmentation/panoptic_fpn_R_50_3x.yaml)) cfg.MODEL.WEIGHTS model_zoo.get_checkpoint_url( COCO-PanopticSegmentation/panoptic_fpn_R_50_3x.yaml) cfg.MODEL.ROI_HEADS.SCORE_THRESH_TEST 0.5 predictor DefaultPredictor(cfg) outputs predictor(img) # img 是 BGR 的 ndarray panoptic_seg, segments_info outputs[panoptic_seg]如果要训练自己的全景分割数据需要先注册 DatasetCatalog 和 MetadataCatalog把标注整理成 detectron2 的 panoptic json 格式里面记录每张图的 segment info 文件路径和类别映射。这段配置里唯一要改的是 SCORE_THRESH_TEST控制实例分支的置信度阈值默认 0.5。全景分割输出是 panoptic_seg 和 segments_info 两个对象前者是每个像素的段 id后者是 id 到类别、thing/stuff 属性的映射。可视化时不要直接给段 id 上色先把 segments_info 里的 category_id 映射到 COCO 类别调色板否则出来的图是一团乱色。4.3 后处理实例与 stuff 冲突时谁覆盖谁如果项目里两个分支是分开训练的最后融合阶段的冲突消解必须自己写。业界通用的规则是「实例优先」stuff 是背景语义实例掩码覆盖上去之后冲突像素以实例为准。下面这段是核心合并逻辑的简化版本背景留 0stuff 语义从 1 开始实例从 offset 开始编号。# panoptic_merge.py语义图 实例图的简化融合 import numpy as np sem_label semantic_result.argmax(0) # (H,W) 每个像素的语义类别 final sem_label.copy() # 先用语义结果打底 for inst_id, mask in enumerate(instance_masks): conflict (sem_label 0) mask # 与 stuff 语义区域重叠 final[mask] offset inst_id # 实例优先写入 final[sem_label 0] 0 # 背景保持 0注意这里的 offset 必须大于语义类别总数比如 stuff 有 20 类offset 取 100避免实例编号和语义编号撞车。这个合并逻辑虽然简化但能讲清楚全景分割框架内部融合模块在做的事设计资料里画数据流图时可以直接照搬。实际框架里还会做细粒度对齐比如把实例掩码缩放到语义图分辨率再叠加思路完全一致。提示mask 的类别索引最好在生成数据集时就固定下来中途改动会让之前保存的权重评估结果全部失效等于白训。5. 图像分割训练避坑五条高频翻车记录与排查路径5.1 损失不降反升学习率与归一化的组合问题现象训练前几个 epoch loss 不但不降还往上涨或者剧烈震荡像随机噪声。 原因最常见是学习率偏大其次是输入没做 ImageNet 归一化。图像分割对输入分布敏感直接除以 255 并不够通道均值和方差不匹配预训练骨干的统计量梯度方向是乱的。 解决先把输入归一化改成 ImageNet 的 mean[0.485, 0.456, 0.406]、std[0.229, 0.224, 0.225]再把学习率降到 1e-4加 5 个 epoch 的线性 warmup。这两步改完绝大多数 loss 震荡问题都会消失不用去换模型结构。5.2 CUDA out of memorycrop、batch 与梯度累积的取舍现象训练跑到第二个 step 就报 CUDA out of memory代码逻辑本身没问题。 原因全图输入加 FPN 多尺度特征显存占用是分类模型的数倍。很多人第一反应是减小 batch但 batch 太小 BN 统计量会飘模型反而不收敛。 解决把输入长边限制在 1024 以内batch 取 2然后用梯度累积凑等效 batch。PyTorch 里每 4 个 step 做一次 optimizer.step()梯度清零延后8G 显存就能模拟 batch 8 的效果。换 24G 卡之前先试这个组合多数场景不用加预算。5.3 预测图全黑或全白类别索引和 colormap 错位现象训练 loss 正常收敛但保存的预测可视化全黑或者全白完全看不出目标在哪。 原因九成是掩码图被当成了三通道 RGB 读取训练时网络看到的是彩色图标签里存的却是可视化颜色另一种是标注时类别从 1 开始代码假设从 0 开始所有类别整体偏移一位。 解决在 Dataset 里加一行检查打印掩码唯一值确认是 0/1/2 这类索引而不是 0/255 这种可视化值。# 排查类别索引训练前打印一次掩码唯一值 print(torch.unique(mask).tolist()) # 期望看到 [0, 1, 2]而不是 [0, 255]我每次新建数据集都先跑这个检查比训练完再看可视化省好几个小时。这个坑最常见的出现时机是拿别人的开源标注转自己的格式转完一定要验证索引范围。5.4 小目标被吞损失加权与多尺度训练现象大目标分割得很好小目标在预测图上消失或者掩码只剩零星几个像素。 原因下采样把小的语义信息稀释了同时 Dice 损失对小目标占比不敏感——一个 10 像素的小目标即使完全预测错对损失的贡献也微乎其微网络没有动力去学它。 解决损失改成 Dice 加带类别权重的交叉熵小目标类别的权重放大 2 到 3 倍训练时做随机尺度变换输入在 0.5 倍到 2 倍之间随机缩放让网络在不同尺度下都见过小目标。两条配合小目标召回率通常能提升 5 到 10 个点代价是训练时间多百分之二三十。5.5 推理慢到没法用半精度、批处理与分辨率下探现象单张图推理要 1 到 2 秒完全没法上线自己也等不下去。 原因FP32 推理、单张输入、输入分辨率过大三个因素叠加。分割模型前向计算量大逐张过模型没有利用批处理并行。 解决先把模型切到半精度model.half() 配合 torch.cuda.amp 的推理上下文速度接近翻倍再把测试集按批处理输入一次喂 8 张最后把输入长边从 2048 降到 1024。三步做完推理耗时能压到原来的四分之一以下。边缘部署再走 ONNX 导出或 TensorRT精度损失靠校准集控制不要在没做前三步的情况下直接上编译优化。6. 指标验证与模型落地mIoU 之外还要看什么6.1 用混淆矩阵定位「哪一类拖后腿」语义分割的验收不能只看 mIoU 一个数。mIoU 是逐类 IoU 的均值某几类特别差会被其他类平均掉看起来数值还行实际产品里那几类全废。我每次训练完都会打印逐类 IoU 和混淆矩阵哪一类被分错、被错分到哪个类别一眼就能定位。逐类 IoU 的计算本身很简单遍历每个类别算交并比就行不需要框架封装。6.2 导出前后指标对比全景分割补看 PQ模型要部署的话先把训练好的权重冻住导出成 TorchScript 或 ONNX在验证集上对比导出前后的 mIoU。差异超过 0.5 个点就检查算子兼容性不要盲目相信导出工具。全景分割还要看 PQPanoptic Quality它是识别质量和分割质量的乘积只有 mIoU 不够——因为全景分割的 stuff 和 thing 是同一张图里评价的一个指标吃不下全部信息。6.3 我的习惯每次实验留一份配置快照我有个习惯每次训练前把学习率、crop、batch、损失权重这些参数存成一份 json和权重文件放同一个目录。调参翻车时回滚到上一个配置只要一分钟不用靠记忆复盘。这套项目从语义分割一路跑到全景分割配置快照是我唯一敢说算「后悔药」的东西。希望帮到你。本文还有配套的精品资源点击获取