图像分割实战:语义、实例与全景分割的选型、训练与部署
发布时间:2026/9/26 11:21:10来源:尧图网络
简介这份资源面向具备一定深度学习基础、希望系统掌握图像分割技术的开发者与学习者聚焦语义分割、实例分割与全景分割三大方向帮助理解像素级分类、实例区分及统一分割框架的核心思路与工程实现。压缩包共4个文件以md说明文档、Python脚本和pro工程配置为主整体约10KB轻量便于快速浏览与二次开发。内容围绕公开数据集预处理、标签转换与分割流程展开涉及U-Net、Mask R-CNN、Panoptic FPN等典型模型的实践脉络可帮助读者梳理从数据准备、模型训练到评估推理的完整链路并借助脚本理解标签处理与工程配置细节。目前已有452人学习适合作为计算机视觉方向课程实践或项目入门的参考素材。1. 从一张图到三类掩码图像分割到底在分什么你手里有一张街景图模型告诉你「这里有一辆车」这叫目标检测但如果它告诉你「这个像素属于车、那个像素属于路、右上角那片属于天空」这就是图像分割。语义分割把所有像素按类别打标签不区分个体实例分割在语义基础上把「车 A」和「车 B」拆开全景分割则把「可数的东西」车、人和「不可数的背景」路、天统一到一张掩码里。这三件事共享同一套深度学习骨架区别只在输出头的设计和标签组织方式。如果你正在做毕设、搭广告牌图像分割系统或者想把医学图像分割落地这篇笔记会从数据准备一路讲到推理部署把能抄的代码和容易翻车的地方都摊开。2. 语义分割、实例分割、全景分割的选型逻辑与数据准备2.1 三类任务的核心差异与模型输出形式语义分割的输出是一张 H×W 的类别索引图每个像素值是一个类别 ID。实例分割的输出是 N 个掩码每个掩码对应一个检测框内的二值区域外加类别和置信度。全景分割的输出则是一张 H×W 的 ID 图每个像素值既编码类别又编码实例序号通常用class_id * 1000 instance_id这种形式拼接。选型时先问自己三个问题第一需不需要区分同一类别的不同个体需要就排除纯语义分割。第二背景区域要不要参与评估要就上全景分割。第三标注预算有多少语义分割标注成本最低实例分割需要多边形或掩码标注全景分割要求标注员同时处理前景实例和背景类别成本最高。常见做法是语义分割用 U-Net 或 DeepLabV3 做基线实例分割用 Mask R-CNN 或 YOLOv8-seg全景分割用 Mask2Former 或 Panoptic DeepLab。如果只是做广告牌图像分割系统语义分割足够如果要做医学图像分割中的细胞计数实例分割更合适。2.2 语义分割数据集制作从标注到 VOC 与 COCO 格式转换语义分割数据集最常见的两种格式是 VOC 和 COCO。VOC 用 PNG 索引图存标签COCO 用 JSON 存多边形。下面这段脚本把 LabelMe 标注的 JSON 转成 VOC 格式的语义分割标签图。import json import numpy as np from PIL import Image import os # 类别名到索引的映射背景固定为0 CLASS_MAP {background: 0, car: 1, person: 2, road: 3} def json_to_voc_mask(json_path, output_path): with open(json_path, r, encodingutf-8) as f: data json.load(f) h, w data[imageHeight], data[imageWidth] mask np.zeros((h, w), dtypenp.uint8) # 按标注顺序反向绘制避免小目标被大区域覆盖 for shape in reversed(data[shapes]): label shape[label] if label not in CLASS_MAP: continue points np.array(shape[points], dtypenp.int32) # 用 PIL 的 polygon 填充比 cv2.fillPoly 更稳定 from PIL import ImageDraw img Image.fromarray(mask) draw ImageDraw.Draw(img) draw.polygon([tuple(p) for p in points], fillCLASS_MAP[label]) mask np.array(img) Image.fromarray(mask).save(output_path) # 批量转换 for file in os.listdir(labelme_jsons): if file.endswith(.json): json_to_voc_mask( os.path.join(labelme_jsons, file), os.path.join(voc_masks, file.replace(.json, .png)) )这段代码的关键参数是CLASS_MAP它决定了标签图的像素值。背景必须占 0因为大多数损失函数默认忽略 0 或把 0 当背景。reversed遍历是为了让先标注的大区域后绘制避免覆盖小目标。转换完成后用PIL.Image.fromarray(mask).save()保存为 PNG不要用 JPG因为 JPG 有损压缩会改变像素值。2.3 实例分割与全景分割的数据组织方式实例分割的数据集通常用 COCO 格式每张图对应一个 JSON里面包含annotations数组每个元素有bbox、segmentation、category_id、iscrowd。全景分割在 COCO 基础上增加了panoptic字段用 PNG 存 ID 图同时 JSON 里记录每个 segment 的类别和 ID 映射。如果你手头只有语义分割标签想转成实例分割可以用连通域分析把同类别的不同区域拆开。下面这段代码演示如何从语义掩码生成实例掩码。import numpy as np from scipy import ndimage def semantic_to_instance(sem_mask, class_id): # 提取指定类别的二值掩码 binary (sem_mask class_id).astype(np.uint8) # 连通域标记structure 决定连通性8连通更宽松 labeled, num ndimage.label(binary, structurenp.ones((3, 3))) instances [] for i in range(1, num 1): inst_mask (labeled i).astype(np.uint8) # 过滤掉面积过小的噪声区域 if inst_mask.sum() 50: continue instances.append(inst_mask) return instancesndimage.label的structure参数控制连通性np.ones((3,3))表示 8 连通适合不规则形状如果目标之间容易粘连改用 4 连通。面积阈值 50 是经验值实际要根据图像分辨率调整太小会保留噪声太大会漏掉小目标。3. 用 PyTorch 跑通语义分割训练从 Dataset 到 DeepLabV33.1 自定义 Dataset 与数据增强的四个必调参数PyTorch 的Dataset需要实现__len__和__getitem__。语义分割的__getitem__要同时返回图像和掩码并且做相同的几何变换。下面是一个可复现的 Dataset 骨架。import torch from torch.utils.data import Dataset import numpy as np from PIL import Image import random class SegDataset(Dataset): def __init__(self, img_dir, mask_dir, img_size512, augmentTrue): self.img_dir img_dir self.mask_dir mask_dir self.img_size img_size self.augment augment self.files [f for f in os.listdir(img_dir) if f.endswith(.jpg)] def __len__(self): return len(self.files) def __getitem__(self, idx): name self.files[idx] img Image.open(os.path.join(self.img_dir, name)).convert(RGB) mask Image.open(os.path.join(self.mask_dir, name.replace(.jpg, .png))) # 统一尺寸用双线性插值处理图像最近邻处理掩码 img img.resize((self.img_size, self.img_size), Image.BILINEAR) mask mask.resize((self.img_size, self.img_size), Image.NEAREST) img np.array(img, dtypenp.float32) / 255.0 mask np.array(mask, dtypenp.int64) # 随机水平翻转图像和掩码必须同步 if self.augment and random.random() 0.5: img np.fliplr(img).copy() mask np.fliplr(mask).copy() # 归一化均值方差用 ImageNet 的 mean np.array([0.485, 0.456, 0.406]) std np.array([0.229, 0.224, 0.225]) img (img - mean) / std img torch.from_numpy(img).permute(2, 0, 1).float() mask torch.from_numpy(mask).long() return img, mask四个必调参数img_size决定显存占用和细节保留512 是精度和速度的平衡点augment控制是否翻转医学图像分割中翻转要谨慎因为器官位置有方向性mean和std必须和预训练模型一致否则微调效果会掉Image.NEAREST用于掩码缩放用双线性会引入不存在的类别 ID。3.2 DeepLabV3 的损失函数与学习率调度DeepLabV3 常用交叉熵损失加辅助损失。下面这段训练循环里ignore_index255忽略未标注像素lr用多项式衰减。import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader model torch.hub.load(pytorch/vision, deeplabv3_resnet50, pretrainedTrue) model.classifier[4] nn.Conv2d(256, NUM_CLASSES, kernel_size1) model model.cuda() criterion nn.CrossEntropyLoss(ignore_index255) optimizer optim.SGD(model.parameters(), lr0.01, momentum0.9, weight_decay1e-4) scheduler optim.lr_scheduler.PolynomialLR(optimizer, total_iters100, power0.9) loader DataLoader(SegDataset(images, masks), batch_size8, shuffleTrue, num_workers4) for epoch in range(100): model.train() for img, mask in loader: img, mask img.cuda(), mask.cuda() optimizer.zero_grad() output model(img)[out] loss criterion(output, mask) loss.backward() optimizer.step() scheduler.step() print(fepoch {epoch}, lr {optimizer.param_groups[0][lr]:.6f})ignore_index255是语义分割的标配标注时未覆盖区域填 255。PolynomialLR的power0.9让学习率缓慢下降比阶梯下降更平滑。batch_size8在 11GB 显存下跑 512×512 输入比较稳如果爆显存先降img_size再降 batch。3.3 用 mIoU 验证语义分割效果mIoU 是语义分割最常用的指标计算每个类别的交并比再平均。下面这段代码在验证集上算 mIoU。def compute_miou(pred, target, num_classes): # pred: [B, H, W] 类别索引, target: [B, H, W] ious [] for cls in range(num_classes): pred_mask (pred cls) target_mask (target cls) intersection (pred_mask target_mask).sum().item() union (pred_mask | target_mask).sum().item() if union 0: continue ious.append(intersection / union) return sum(ious) / len(ious) if ious else 0.0注意union 0时跳过否则会除零。如果某个类别在验证集里没出现跳过是合理的但要在日志里记录哪些类别被跳过了否则 mIoU 会虚高。4. 实例分割与全景分割的落地路径Mask R-CNN 与 Mask2Former4.1 用 torchvision 跑通 Mask R-CNN 实例分割Mask R-CNN 在 torchvision 里有现成实现替换box_predictor和mask_predictor的类别数即可。import torchvision from torchvision.models.detection.faster_rcnn import FastRCNNPredictor from torchvision.models.detection.mask_rcnn import MaskRCNNPredictor def get_mask_rcnn(num_classes): model torchvision.models.detection.maskrcnn_resnet50_fpn(pretrainedTrue) # 替换框分类头 in_features model.roi_heads.box_predictor.cls_score.in_features model.roi_heads.box_predictor FastRCNNPredictor(in_features, num_classes) # 替换掩码头 in_features_mask model.roi_heads.mask_predictor.conv5_mask.in_channels model.roi_heads.mask_predictor MaskRCNNPredictor(in_features_mask, 256, num_classes) return modelnum_classes要包含背景类所以实际类别数加 1。Mask R-CNN 的输入是List[Tensor]每张图尺寸可以不同但为了 batch 训练通常用GeneralizedRCNNTransform统一缩放。训练时targets需要boxes、labels、masks三个字段masks是[N, 1, H, W]的浮点张量。4.2 全景分割用 Mask2Former 的推理与后处理Mask2Former 把全景分割统一成掩码分类问题输出pred_masks、pred_logits、pred_boxes。后处理时用匈牙利匹配或阈值筛选把掩码分配到stuff和thing两类。from transformers import Mask2FormerForUniversalSegmentation, Mask2FormerImageProcessor processor Mask2FormerImageProcessor.from_pretrained(facebook/mask2former-swin-base-coco-panoptic) model Mask2FormerForUniversalSegmentation.from_pretrained(facebook/mask2former-swin-base-coco-panoptic) inputs processor(imagesimage, return_tensorspt) with torch.no_grad(): outputs model(**inputs) # 后处理得到全景分割图 result processor.post_process_panoptic_segmentation(outputs, target_sizes[image.size[::-1]])[0] panoptic_map result[segmentation] # H×W 的 ID 图 segments_info result[segments_info] # 每个 segment 的类别和 IDpost_process_panoptic_segmentation会自动处理thing和stuff的合并segments_info里isthing字段区分两者。如果显存不够把swin-base换成swin-tiny但小目标分割质量会下降。4.3 三类分割任务的评估指标对照任务常用指标计算方式注意事项语义分割mIoU每类交并比平均忽略未标注像素实例分割AP[0.5:0.95]掩码 IoU 阈值下的平均精度按类别和面积分组全景分割PQ识别质量 SQ × 分割质量 RQ分别算 thing 和 stuffPQ 的计算里SQ 是匹配上的 segment 的平均 IoURQ 是匹配率。如果 PQ 低但 SQ 高说明漏检多如果 SQ 低说明掩码边界不准。5. 避坑与排查图像分割训练里最常见的五个翻车现场5.1 损失不下降mIoU 卡在 0.1 以下现象训练几个 epoch 后 loss 震荡mIoU 几乎不变。原因通常是类别不平衡背景像素占 90% 以上模型学会全预测背景。解决用WeightedCrossEntropyLoss给稀有类别加权或者改用 Dice Loss 加交叉熵的组合。权重按类别频率的倒数设置但不要超过 10否则训练不稳定。5.2 验证集 mIoU 很高推理时掩码全是噪声现象验证集指标 0.8实际跑单张图输出彩色噪点。原因多半是归一化不一致训练时用了 ImageNet 均值方差推理时忘了减均值除方差。解决把预处理封装成一个函数训练和推理共用避免手写两套。另外检查model.eval()和torch.no_grad()是否加上BatchNorm 在训练模式下会用当前 batch 的统计量单张推理时统计量偏差极大。5.3 实例分割的掩码边界比检测框大一圈现象Mask R-CNN 输出的掩码超出目标实际边界。原因mask_predictor的输出是 28×28上采样到原图时用了双线性插值边界被平滑放大。解决在post_process里对掩码做阈值化通常取 0.5然后用paste_masks_in_image的padding参数控制边界。如果还大把MaskRCNNPredictor的dim_reduced从 256 降到 128减少过拟合。5.4 全景分割的 thing 和 stuff 类别 ID 冲突现象全景 ID 图里同一像素被分配了两个类别。原因后处理时thing和stuff的掩码没有做非极大值抑制。解决先按置信度排序逐个写入 ID 图已占用的像素跳过。Mask2Former 的post_process_panoptic_segmentation内部已经做了但如果自己写后处理必须加这一步。5.5 多卡训练时 mIoU 比单卡低现象单卡 mIoU 0.75四卡 DDP 后掉到 0.6。原因学习率没随 batch size 线性放大或者 BatchNorm 在 DDP 下同步统计量导致小 batch 不稳定。解决lr乘以卡数或者改用SyncBatchNorm。如果还不行把batch_size设成每卡 4 以上太小就换GroupNorm。6. 把训练好的分割模型塞进推理管线ONNX 导出与量化提速训练完的模型要落地第一步是导出 ONNX。下面这段代码把 DeepLabV3 导出并做动态量化。import torch.onnx model.eval() dummy torch.randn(1, 3, 512, 512).cuda() torch.onnx.export( model, dummy, deeplabv3.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}}, opset_version11 ) # 动态量化把 Conv 和 Linear 的权重转成 int8 import torch.quantization quantized_model torch.quantization.quantize_dynamic( model, {nn.Conv2d, nn.Linear}, dtypetorch.qint8 ) torch.save(quantized_model.state_dict(), deeplabv3_quantized.pth)opset_version11兼容大多数推理引擎dynamic_axes让 batch 维度可变。动态量化对 Conv 的加速有限因为 Conv 的计算瓶颈在乘加int8 的收益不如 Transformer 明显。如果追求极致速度用 TensorRT 做 FP16 或 INT8 校准但校准集要覆盖所有类别否则稀有类别精度会崩。验证量化后精度的方法在验证集上跑一遍量化模型对比 mIoU 下降是否超过 2 个百分点。如果超过只量化全连接层保留卷积层为 FP32。我一般会保留一份 FP32 模型做兜底量化模型只在边缘设备上用。最后说个血泪教训导出 ONNX 前一定要把模型设为eval()并且用torch.no_grad()包住导出过程。我有一次忘了设 evalBatchNorm 的 running_mean 被更新导出的模型推理结果和训练时完全对不上排查了一整天才发现是这行代码。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网