ARTICLE DETAIL

资讯详情

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

图像分割实战指南:语义、实例与全景分割从训练到部署

图像分割实战指南:语义、实例与全景分割从训练到部署 简介一份覆盖语义分割、实例分割与全景分割的图像分割Python项目适合计算机视觉与深度学习方向的开发者、学生及科研人员可直接用于毕设或课程设计参考。压缩包共5个文件含3个Markdown说明文档、1个Python脚本与1个工程文件整体约11KB结构紧凑便于快速理解项目逻辑。目前已有206人学习下载适用于相关专业在校生、教师及企业算法工程师。内容涵盖基于深度学习方法的分割思路整理、CCF遥感图像分割赛题工程参考含相关预处理工程与配置以及标签转单通道等数据处理脚本可帮助读者打通从预处理、模型搭建到工程部署的完整链路亦可作为项目答辩或论文写作的基础素材。1. 图像分割三兄弟语义、实例、全景先分清你要哪个同样是“分割”三个词背后是三套完全不同的技术选型。语义分割做像素级分类把猫和狗各涂一片色就叫完成实例分割要在这个基础上把两只黏在一起的猫拆成猫1和猫2全景分割更狠要求画面里每一块像素都有归属——人、车这种可数的“东西”各算各的天空、道路这种不可数的“材料”也要覆盖到位。这个区别直接决定你下载来的深度学习图像分割源码能不能用拿语义分割的项目去接实例分割的需求改代码的痛苦远大于重新训练一个模型。图像分割最密集的应用场景集中在医学影像病灶区域提取、自动驾驶车道线和可行驶区域、遥感解译、电商抠图和工业质检。适合谁适合已经跑通过图像分类或目标检测、现在需要输出像素级结果的开发者和学生。如果你刚接触Python和深度学习本文所有命令从环境配置开始按步骤来如果你已经跑过UNet重点看第4章的排错和第5章的部署加速那些才是生产环境里真正卡人的地方。2. 环境与数据从零跑通图像分割最小工程的硬指标2.1 先固化Python环境四个包版本能救命图像分割项目最怕环境不一致。同一个UNet代码在我机器上loss正常下降换台机器就报shape mismatch多半是库版本漂移。我一般会用conda单独建一个虚拟环境Python版本直接锁定3.10不要用系统自带的Python也别用最新版有些分割库对Python 3.12的适配还不完善。conda create -n seg python3.10 -y conda activate seg pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install opencv-python albumentations tqdm tensorboard pip install pycocotools提示--index-url这里指定的是CUDA 11.8的预编译版本。如果你用的是NVIDIA 30系以后的显卡CUDA 11.8是兼容性最好的选择如果你不装GPU版直接pip install torch torchvision走CPU版本也可以跑通小数据集但把batch size调小一些。装完立刻验证GPU是否真的可用这一步能排查掉80%的后续报错import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0) if torch.cuda.is_available() else CPU mode)如果cuda.is_available()返回False优先检查你安装的torch是不是CPU版本用pip list | grep torch看版本号里有没有cu后缀其次是显卡驱动版本太旧在命令行用nvidia-smi看驱动支持的CUDA版本号必须大于等于你在pip里指定的CUDA版本。这一步很多人翻车最常见的原因就是torch版本和驱动对不上。另外强烈建议装一个segmentation-models-pytorch库它把UNet、DeepLabV3、FPN等主流分割模型封装成了几行代码能调用的接口做对比实验时能省下大量改模型结构的时间。但注意这个库更适合快速验证想法生产环境部署时还是要把模型结构固定下来避免库升级导致推理结果变化。2.2 数据准备标注格式与目录结构决定后续所有代码图像分割的数据格式主要分两种一种是小数据集常用的单通道PNG掩码图每个像素的灰度值就是类别编号另一种是COCO格式的JSON标注文件多边形坐标需要运行时转换为掩码。两种格式差异很大下代码前先看清楚项目用的是哪种。推荐目录结构按以下方式组织约定一致后训练、验证、推断脚本都不用改路径参数dataset/ images/ train/ # 原图jpg或png val/ masks/ train/ # 语义分割掩码单通道png val/ annotations/ # 实例/全景分割的json标注如用COCO格式如果拿到的是LabelMe标注的JSON文件需要先转换为掩码PNG。这里给一个转换脚本的核心逻辑用opencv从多边形填充掩码import json import numpy as np import cv2 import os def labelme_json_to_mask(json_path, img_shape, label_map): 把labelme的polygon标注转成单通道掩码 背景为0每个标注对象按label_map映射到对应类别编号。 with open(json_path, r, encodingutf-8) as f: data json.load(f) mask np.zeros(img_shape[:2], dtypenp.uint8) # 生成空掩码 for shape in data[shapes]: label shape[label] class_id label_map.get(label, 0) # 不认识的标签归为背景 points np.array(shape[points], dtypenp.int32) cv2.fillPoly(mask, [points], colorclass_id) # 多边形填充 return mask # 假设label_map {tumor: 1, organ: 2} # 调用时对images/train每张图找到同名json即可逻辑说明label_map是标签名到类别编号的映射表语义分割要求掩码值从0开始且连续因为损失函数计算交叉熵时类别索引就是像素值。cv2.fillPoly用多边形顶点坐标填充掩码区域如果标注里同一个类别有多个对象比如多张桌子多次调用fillPoly累加即可相同类别编号会自然融合。这个转换有四个高频坑一是确认图像通道顺序是BGR还是RGBlabelme原始图像读取后是BGR但训练框架里预处理通常转RGB掩码转换用的是坐标几何关系不受通道顺序影响但别忘了检查二是多边形顶点坐标的坐标系原点在左上角与图像像素坐标一致不需要额外偏移三是某些标注工具导出的是归一化坐标需要乘图像宽高还原四是掩码必须保存为PNG格式不要用JPEG保存掩码有损压缩会篡改类别编号损失函数对错误的类标一无所知却会学出奇怪的东西。2.3 第一次训练从基线到看清损失曲线第一次跑模型不需要花哨的结构用UNet配合一个简单Dataset类就能建立基线。Dataset的核心是做好三个同步图像读进来是什么格式、掩码读进来是什么格式、数据增强对两者做同样变换。import torch from torch.utils.data import Dataset import cv2 import albumentations as A class SegDataset(Dataset): def __init__(self, img_dir, mask_dir, img_size(256, 256), augmentFalse): self.img_paths sorted(os.listdir(img_dir)) self.mask_dir mask_dir self.img_size img_size self.augment augment # 训练时用albumentations做随机翻转、缩放必须保证图和掩码同变换 self.transform A.Compose([ A.RandomResizedCrop(256, 256, scale(0.5, 1.0)), A.HorizontalFlip(p0.5), A.RandomBrightnessContrast(p0.2), ]) def __len__(self): return len(self.img_paths) def __getitem__(self, idx): img_path self.img_paths[idx] img cv2.imread(os.path.join(self.img_paths_dir, img_path)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask cv2.imread(os.path.join(self.mask_dir, img_path.replace(.jpg, .png)), cv2.IMREAD_GRAYSCALE) if self.augment: augmented self.transform(imageimg, maskmask) img, mask augmented[image], augmented[mask] # 转为tensor像素归一化到[0,1]掩码保持long类型 img torch.from_numpy(img).permute(2, 0, 1).float() / 255.0 mask torch.from_numpy(mask).long() return img, mask逻辑说明RandomResizedCrop同时作用于img和mask保证了空间对齐HorizontalFlip是分割任务最常用的增强因为分割标签的语义不受水平翻转影响。permute(2,0,1)把HWC转为CHWPyTorch卷积网络默认输入是通道在前。掩码转换时用long()是因为交叉熵损失的target必须是整型。训练时使用交叉熵损失配合AdamW优化器学习率初始值1e-4不要用默认的1e-3分割任务的像素级预测对学习率更敏感梯度爆炸的可能性更大。训练脚本循环里每过一定epoch做一次验证记录mIoU和loss曲线看到loss降到一定程度后不再下降再开始调整模型结构或数据增强策略不要一上来就换模型。3. 三类分割全都要会语义、实例、全景的最小可靠实现3.1 语义分割UNet的跳跃连接为什么对小目标友好UNet是语义分割里最稳健的基线模型尤其适合医学图像分割这类样本量不大的场景。它的核心设计是编码器-解码器结构加跳跃连接编码器逐层下采样提取高层语义解码器逐步恢复空间分辨率跳跃连接把编码器的高分辨率特征直接拼接到解码器同层让网络在预测像素类别时同时看到细节和语义。一个精简UNet的核心结构如下完整版比这个多几层block但原理一致import torch.nn as nn class DoubleConv(nn.Module): 两个卷积BNReLUUNet的基本构建块 def __init__(self, in_ch, out_ch): super().__init__() self.conv nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue) ) def forward(self, x): return self.conv(x) class UNet(nn.Module): def __init__(self, in_ch3, num_classes2): super().__init__() # 编码器 self.enc1 DoubleConv(in_ch, 64) self.enc2 DoubleConv(64, 128) self.pool nn.MaxPool2d(2) # 解码器 self.up2 nn.ConvTranspose2d(128, 64, 2, stride2) self.dec2 DoubleConv(128, 64) # 跳跃连接后通道数翻倍 # 输出层 self.out nn.Conv2d(64, num_classes, 1) def forward(self, x): # 编码 e1 self.enc1(x) # 256x256 e2 self.enc2(self.pool(e1)) # 128x128 # 解码 d2 self.up2(e2) # 恢复空间尺寸 d2 torch.cat([d2, e1], dim1) # 跳跃连接拼通道 d2 self.dec2(d2) return self.out(d2)逻辑说明DoubleConv里每个卷积后接BatchNorm2d和ReLUBN在batch size较小时容易不稳定所以UNet训练batch size至少设在8以上。ConvTranspose2d是转置卷积上采样也可以用双线性插值上采样再卷积后者参数更少且不容易产生棋盘效应。torch.cat在通道维度拼接这是跳跃连接的关键跳跃连接让解码器直接访问编码器的精细边缘信息对分割结果有质的提升。参数设置上有三个要点第一输入尺寸按训练时的尺寸固定推理时图像尺寸如果与训练尺寸不一致最好做resize而非直接输入因为UNet全卷积结构理论上不限制输入尺寸但BN层统计数据是基于训练尺寸的第二num_classes要和你的掩码类别数一致二分类问题输出为1个通道加sigmoid还是2个通道加softmax两种写法都在用但推荐后者收敛更稳定第三UNet系列找backbone时常用ResNet34或EfficientNet做编码器segmentation-models-pytorch里直接支持Unet(resnet34, encoder_weightsimagenet)这种方式加载预训练权重。3.2 实例分割Mask R-CNN在torchvision里的快速落地实例分割相比语义分割多出一个目标检测分支每个实例要输出边界框、类别和像素掩码。Mask R-CNN是经典方案在Faster R-CNN基础上增加了一个并行的掩码分支RoI Align操作替代了RoI Pooling解决了特征图对齐的像素偏差。torchvision直接提供了预训练模型这是快速落地的最佳路径import torchvision from torchvision.models.detection import maskrcnn_resnet50_fpn # 第一次调用会自动下载预训练权重约178MB model maskrcnn_resnet50_fpn(pretrainedTrue, num_classes91) # 如果要在自己的数据集上微调把分类头换掉 from torchvision.models.detection.faster_rcnn import FastRCNNPredictor # 假设你的数据只有1个类加上背景共2类 num_classes 2 in_features model.roi_heads.box_predictor.cls_score.in_features model.roi_heads.box_predictor FastRCNNPredictor(in_features, num_classes) # 掩码分支的输出通道数也要改 from torchvision.models.detection.mask_rcnn import MaskRCNNPredictor in_features_mask model.roi_heads.mask_predictor.conv5_mask.in_channels model.roi_heads.mask_predictor MaskRCNNPredictor(in_features_mask, 256, num_classes)逻辑说明maskrcnn_resnet50_fpn的预训练权重是在COCO上训的如果目标域差异很大比如医学影像建议用pretrainedTrue做初始化然后全量微调如果目标域接近COCO比如通用物体可以冻结backbone只训练检测头。替换分类头时cls_score.in_features和conv5_mask.in_channels分别读取原模型对应层的输入维度替换后原有权重参数会被随机初始化只有这些层的梯度会更新。实例分割的数据集格式一般整理成字典列表每张图对应一个字典包含boxesNx4的tensor格式是x1, y1, x2, y2、labelsN维tensor从1开始0保留给背景、masksNxHxW的二值tensor。训练时数据加载需要自定义collate函数因为每张图的实例数不同不能简单堆叠成一个batch。torchvision官方文档给出的collate函数就是把每个样本的字典原样打包成list实测最简单可靠。3.3 全景分割把thing和stuff统一到一个id空间全景分割的实现思路是把实例分割thing类和语义分割stuff类的结果融合thing类用实例分割来预测stuff类用语义分割来预测然后将两类结果统一编码到同一个标签空间。一种常见的工程实现是使用detectron2的Panoptic FPN模型它用一个共享的backbone网络同时输出语义分割和实例分割结果。安装detectron2在Windows上有些麻烦官方建议在Linux环境编译或用预编译wheel包。下面给出推理的核心流程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格式的numpy数组 panoptic_seg, segments_info outputs[panoptic_seg] # panoptic_seg是HxW的uint8张量每个像素的编码方式是 # category_id * segment_id segment_id的复合编码需要用segments_info解码 for seg in segments_info: print(seg[category_id], seg[isthing], seg[area])逻辑说明panoptic_seg里每个像素的值不是直接对应类别编号而是一个编码后的id必须配合segments_info才能转换。segments_info里包含每个分割区域的category_id、isthing标记和面积。实际项目中要把panoptic_seg可视化需要遍历segments_info根据category_id查类别名到颜色的映射然后把掩码区域填充为对应颜色。这一点是新手最容易踩坑的地方千万别直接把raw输出当作类别掩码用。全景分割对算力要求最高如果业务只关心前景物体不需要做全景分割如果确实需要全场像素覆盖也要确认stuff类的标注质量因为stuff类区域面积大但标注粗糙一旦训练数据里马路和路沿的边界标得含糊模型输出就会在边缘处出现大片混叠看起来比语义分割结果更糟糕。4. 图像分割避坑实录五个高发问题的现象、原因与解决4.1 训练时loss为Nan常见于前几个epoch就崩掉现象训练刚开始或中途loss值突然变成NaNtensorboard曲线断崖式消失GPU显存占用率骤降。原因最常见的是学习率太大导致梯度爆炸其次是数据里有NaN的掩码值或者图像全黑还有一部分是混合精度训练和某些归一化层冲突。解决先把学习率降到1e-5跑10个epoch确认loss可以下降再逐步恢复。同时检查数据加载时是否过滤了不可读的图片用np.isfinite(img).all()验证输入数据。混合精度训练的GradScaler不要一开始就用先在单精度下跑通再启用。4.2 验证集mIoU很高但部署到真实图片上一塌糊涂现象训练集和验证集都是同一分布的数据指标好看部署到用户拍摄的图片上分割结果出现大片缺失或错切。原因数据增强太弱网络对光照变化、遮挡、相机视角变化过拟合了。很多开源数据集拍摄条件统一训练出的模型很容易把背景颜色当成判别特征。解决增加强数据增强比如随机亮度对比度、高斯噪声、随机仿射变换和弹性形变。把训练时用的增强方式记录到配置文件里部署时无效。另外在验证集里混入一些域外图片作为烘干测试即使不计算指标目视检查也能看出泛化问题。4.3 一个小目标被漏检或者多个小目标融合成一整块现象小物体分割不出来或者相邻的多个同类小物体在掩码里连成一片视觉上是一整块。原因连续下采样过程中小目标信息丢失特征图上只有几个像素响应或者损失函数按像素平均小目标在交叉熵中的占比极低网络优化时优先保证大目标。解决一是用DiceLoss或FocalLoss替代纯交叉熵DiceLoss对小目标的固有面积占比敏感度更低二是在数据加载时做随机裁剪把小目标相对放大三是选择更高分辨率的输入比如把训练尺寸从256提升到512显存不够就用torch.utils.checkpoint梯度检查点技术来换取显存。4.4 CUDA out of memory 高频发生改小batch size后卡死了现象训练或推理时显存不足报错把batch size减到1还是不够。原因显存除了保存输入、输出和中间激活值还把整个计算图存下来用于反向传播。全卷积网络即使batch size为1如果输入分辨率为1024每一层的激活值累积起来照样能吃满12GB显存。解决先用torch.cuda.max_memory_allocated()统计显存峰值定位是哪个阶段消耗最大。实践中最有效的手段是开启torch.cuda.amp.autocast()混合精度训练显存直接减半输入尺寸过大的用CenterCrop或Resize限制到模型能承受的范围。推理阶段用torch.no_grad()包裹同时用model.eval()关闭dropout和BN的统计更新这两步不做显存占用差距可达30%以上。4.5 全景分割结果里thing类边界和stuff类边界重叠或留下缝隙现象panoptic输出里人形区域边缘出现了细小的背景碎片缝隙或者车辆边缘被“吃进去”一圈。原因thing类和stuff类的预测是分开的后处理融合时没有做冲突消解。一些像素同时被实例分割判定为“车辆”又被语义分割判定为“道路”融合规则如果简单按类别优先级覆盖就会出现边缘不整齐。解决对照官方实现的merge_semantic_and_instance函数规则是实例分割的置信度高于阈值时直接覆盖语义分割结果低于阈值时遵从语义分割。这个阈值的选取需要调参一般设在0.3到0.5之间日志记录里要输出冲突像素数量方便判断阈值是否过严或过松。5. 部署前必须做的最后两步ONNX导出与预测可视化模型训练完不等于项目结束生产环境最常用的是ONNX Runtime或TensorRT推理。把PyTorch模型导出为ONNX格式需要固定输入尺寸否则动态维度会让导出的图变得复杂且性能下降。import torch import onnxruntime as ort # 假设model是训练好的UNet输入尺寸256x256 model.eval() dummy_input torch.randn(1, 3, 256, 256) torch.onnx.export( model, dummy_input, unet.onnx, opset_version12, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}} ) # ONNX Runtime推理 ort_session ort.InferenceSession(unet.onnx, providers[CUDAExecutionProvider]) ort_inputs {input: img_numpy.astype(np.float32)} outputs ort_session.run(None, ort_inputs)[0] # 拿到分割概率图提示导出ONNX前务必用torch.jit.trace先跟踪一遍模型确认没有控制流依赖输入数据的分支否则导出会报错或得到错误结果。UNet这类全卷积模型结构固定导出一帆风顺如果模型里用了基于数据检测的动态判断逻辑就要改写模型结构再导出。ONNX导出成功后我的调试习惯是先写一个可视化脚本把每张验证图片的预测掩码叠加到原图上保存为一张带原始图、标签图、预测图三栏的拼图。这个脚本写好后每次调参训练完直接跑一遍用眼睛看图而不是只看mIoU数字。mIoU高并不能保证边缘细节正确肉眼扫一遍比任何指标都能更快发现模型在局部结构上的问题。关于ONNX的优化如果显存充足使用TensorRT把模型精度转成FP16推理延迟能降到原来的三分之一左右。TensorRT的engine文件与GPU型号强绑定换卡就要重新生成。如果你的项目只支持CPU推理ONNX Runtime的CPU线程数默认会占满所有核在并发场景下要设置intra_op_num_threads限制线程数否则多个推理请求会互相抢CPU导致整体吞吐量不增反降。这个细节是我在部署时踩过的坑单路推理延迟很好看并行一压测就暴露出来。图像分割项目沿用至今的个人习惯是先做数据质量检查再做模型调优。拿到任何开源代码先跑一个batch的图片和掩码可视化出来人工核对确认标签对齐了再开始训练。很多问题追根溯源不是模型结构不行而是数据从标注到掩码的转换那一环出了错。希望这些经验能帮你把项目的踩坑时间压缩下来一次跑通。本文还有配套的精品资源点击获取
返回列表