尧图精选

DeepLabV3人物抠图实战:语义分割原理、训练与批量交付

🕒 发布时间:2026/10/2 14:51:12 📁 来源:尧图网络
简介这是一份基于PyTorch官方DeepLabV3实现人物抠图语义分割的完整代码包适合有一定深度学习基础、希望快速上手图像分割或复现二分类抠图项目的开发者。资源采用deeplabv3_resnet50作为默认模型同时可轻松切换deeplabv3_resnet101、deeplabv3_mobilenet_v3_large进行对比实验工程中集成了wandb可视化监控、交叉熵与dice_loss组合损失函数并包含训练、验证与预测的完整流程可直接在自有数据集上迁移使用。压缩包共3453个文件以3411张png训练/测试图片为主另有15个py脚本、20个pyc文件、Dockerfile、Linux启动脚本等配置支撑整体容量约990.96MB结构上按数据、代码、模型与容器编排分层便于部署复现。该资源已吸引1260人学习适合作为入门语义分割、调参及损失函数优化的参考工程。1. 先把话说明白deeplabv3人物抠图到底解决什么问题用PS或者GIMP钢笔工具去抠人物头发抠一张图少说要半小时碰到细碎发丝更是体力活。OBS的AI智能抠图插件、网页版PS的一键抠图虽然快但模型不透明批量出图也没法统一微调。Deeplab实战里的deeplabv3是DeepLab系列里落地性价比最高的一版语义分割模型它把“找出图中所有属于人物的像素”变成端到端分类问题输出的概率图稍作后处理就是现成的alpha通道。这篇笔记会把原理、数据集准备、训练参数和踩坑点一次讲完。适合给商品图、直播背景和短视频批量做人物分离的从业者也适合想拿语义分割练手、要求结果能直接交付的开发。2. 选型逻辑ASPP多尺度为什么能顶住复杂背景backbone怎么挑新人做人物分割第一反应通常是U-Net或者Mask R-CNN这两个不是不行而是在“批量抠图”这个具体诉求下绕了远路。下面把对比讲透再落到backbone选型和预训练权重的迁移策略上。2.1 U-Net和Mask R-CNN为什么没进决赛圈U-Net在医学影像分割里口碑很好但它的设计假设是目标尺度相对集中、结构边界清晰。人物抠图不一样一张电商图里模特占画面60%一张街拍里路人只有几十像素高尺度跨度经常超过20倍。U-Net靠encoder-decoder加skip connection逐层传细节没有显式的多尺度分支小目标在几次下采样之后特征基本被抹平后面decode阶段想找回来也难。Mask R-CNN走的是实例分割路线输出是“第几个目标的mask”。一张三人合照会产出三张mask你得先判定哪个是人再把重叠区域按置信度合并最后拼成一张语义图。这个检测加实例编号的流程在纯抠图场景里是纯开销。更麻烦的是Mask R-CNN的mask分支在RoIAlign之后固定输出28x28的小图边缘细节先天不足拿来做头发丝级别的抠图还得额外加一套refinement网络。deeplabv3走语义分割路线输出就是整张图每个像素在“背景/人物”两类上的概率一把梭哈到底。它的ASPP模块用多个并行分支抓不同尺度正好应对“人离镜头远近”带来的尺度变化。这也是为什么它在COCO和VOC上的分割指标长期是性价比最高的选择之一。2.2 ASPP的四个分支和膨胀率是怎么让模型“既见森林又见树木”的语义分割里的“感受野”决定了一个像素能参考多大范围的上下文。普通3x3卷积感受野只有3x3要堆很多层才能扩大但分辨率会一路下降。DeepLabV3的答案是不堆层用空洞卷积。空洞卷积在不增加参数量的前提下把卷积核的采样点之间插进空洞rate6时有效感受野等效为13x13rate12是25x25rate18是37x37。ASPP在backbone最后一层特征图上并行接四路一路1x1卷积保留局部细节三路3x3空洞卷积rate分别取6、12、18对应中近远三个尺度的上下文再加一路全局平均池化把整张图的全局信息浓缩成向量再双线性插值回原尺寸。五路输出在通道维拼接过一个1x1卷积融合最后上采样到输入分辨率。这个结构好在哪远处的小人他的像素语义要依赖37x37甚至全局上下文才能判对近处的大头发丝边缘又需要小rate分支保住的原始细节。四路并联等于把“看不清就多看周围”和“看得清就死抠细节”同时做到了。这也是deeplabv3当年在VOC上拿state-of-the-art的核心原因到现在做人物抠图它依然是离线批量场景下的稳定默认选项。2.3 backboneResNet101还是MobileNetV2离线批量和实时视频分开选backbone决定了下游特征的质量和计算量。我平时在两个选择里权衡ResNet101追求精度MobileNetV2追求速度。下面是常用的选型对照数值是社区里常见水平不同数据分布会浮动几个点但量级差不了太多。backbone常见VOC mIoUOS16单帧512推理耗时RTX 3060显存占用batch4适合场景ResNet10179左右约90ms约9GB离线批量抠图、商品图精修ResNet5077左右约60ms约6GB精度和速度的中间档MobileNetV272左右约25ms约2GB实时视频流、低显存设备选型建议很简单如果任务是在晚上跑一批一千张图第二天交付就无脑ResNet101如果要做直播实时背景替换MobileNetV2才扛得住帧率。我自己的血泪经验是拿ResNet101去做实时推流结果是GPU风扇狂转、帧率不到15fps最后乖乖换MobileNetV2。精度掉几个点换来的是能用的延迟这个交换在实时场景里非常值。还要注意显存的隐性开销torchvision的deeplabv3默认开了aux_loss训练时会多一个辅助分类头显存占用比纯ASPP高15%左右。显存紧的话可以关掉但代价是深层的梯度回流变弱小目标收敛会慢一点。我一般保留aux_loss把batch size降一档来换。2.4 预训练权重和迁移策略从COCO权重出发的微调路线不建议从随机初始化开始训。语义分割需要大量数据从头训一个ResNet101ASPP在单卡上要一两周。常见做法是加载在COCO上预训练过的deeplabv3_resnet101权重然后把最后一层分类卷积从21类改成2类在人物数据上微调。COCO上训练过的backbone已经见过足够多的自然图像纹理迁移过来后几十个epoch就能收敛到可用水平。我一般分两步走第一步冻结backbone所有层只训练分类头和ASPP用基础学习率0.007跑2到3个epoch让随机初始化的分类头先适应新任务第二步解冻全部层把学习率降到0.001用poly衰减再训30到50个epoch。这样做的理由是如果一开始全量微调随机初始化的分类头会产生巨大梯度把预训练好的backbone权重冲乱loss曲线一开局就崩。先稳住头再慢慢调全身曲线会好看很多。很多loss飞上天的翻车现场十有八九是上来就全量微调导致的。3. 准备人物分割数据集VOC转二分类、软边界和zip解压那些事训练deeplabv3最常见的路线是在公开分割数据集上微调很少有人从零标注。原因在GT——人物分割GT难画头发丝和衣服边缘一张张描要人命。公开数据集选型、标签映射、预处理这三个环节决定了下游80%的效果这一章按顺序拆开讲。3.1 数据源怎么选VOC2012、Cityscapes、Supervisly Person数据集person类标签标注方式分辨率适合场景VOC201215polygon多为500x375左右入门、类别全但person样本量一般Cityscapes11trainId像素级语义2048x1024街景人物多且小车辆遮挡多Supervisly Personperson高质量像素级不等纯人物分割边缘标注更仔细VOC2012是入门首选但注意它的person样本主要集中在train和val里大约有几千张含人的图其中不少人物占画面比例小、遮挡多。Cityscapes的GT是2048x1024边缘标注精度高但全是街景电商模特图、室内图这种数据分布它没见过直接拿来微调会有明显的域差异。如果你要做的产品面向商品图最好用VOC的person加Cityscapes的person混合训练再混入一部分自己业务域的图。Supervisly Person标注质量好只是量不大适合做补充集而不是主力。数据集下载下来通常是zip包解压后先检查目录结构。常见布局是JPEGImages放原图、SegmentationClass放GT两者文件名一一对应。我习惯先写一个脚本统计GT里的类别集合确认person的索引值是15而不是别的数再做映射。3.2 标签映射21类VOC标签转成背景/前景二分类的脚本VOC的GT是单通道P模式png像素值0表示背景15表示person其余是其他类别。训练抠图模型我们要把它们映射成0/1二分类。# 将VOC的21类GT转为二分类(0背景,1人物) import numpy as np from PIL import Image def voc_person_to_binary(gt_path, out_path, person_id15): gt np.array(Image.open(gt_path)) # HxW值是类别索引 binary np.where(gt person_id, 1, 0).astype(np.uint8) Image.fromarray(binary * 255).save(out_path)逻辑说明先用PIL打开GTnp.array取出的就是P模式的调色板索引即类别id。gtperson_id生成布尔矩阵转成uint8后乘255得到0和255的二值图方便后续可视化检查。参数person_id在VOC里固定为15如果换成Cityscapes要改成它的trainId11千万别拿cityscapes的原始id来用。强调一个坑这里不能用cv2.imread读GT。cv2会把P模式png展开成RGB三通道np.array(gt)之后形状变成HxWx3gt15永远为False全图都成了背景。我踩过一次排查半小时才发现是读图方式的问题后来专门在脚本里加了个断言检查GT像素值集合是否合理。3.3 数据增强与边界软化别让模型去拟合一个本身不准的GT数据增强用随机crop、水平翻转、色彩抖动、随机缩放。裁剪尺寸和模型的输出步长要匹配deeplabv3在OS16下512x512的crop能覆盖enough上下文也不会把大人物切得太碎。# 数据增强管道的关键部分 import cv2, random import numpy as np def train_augment(img, gt): # 先随机缩放0.5~1.5倍再crop 512x512 h, w gt.shape[:2] scale random.uniform(0.5, 1.5) img cv2.resize(img, (int(w * scale), int(h * scale))) gt cv2.resize(gt, (int(w * scale), int(h * scale)), interpolationcv2.INTER_NEAREST) # 随机crop h2, w2 gt.shape[:2] y random.randint(0, max(0, h2 - 512)) x random.randint(0, max(0, w2 - 512)) return img[y:y512, x:x512], gt[y:y512, x:x512]逻辑说明GT做resize时必须用INTER_NEAREST不能用双线性否则0/1之间会出现灰色过渡等于引入了本来不存在的标签。随机crop时要注意h2和w2可能小于512处理方式是先把短边pad到512或者重新采样一张图。另一个细节是crop区域如果完全没有person这个样本就是纯背景可以在采样时过滤掉否则会拉低训练效率。然后是边界软化。VOC的polygon标注在头发丝、手指缝处是“一笔切过去”GT边缘本身就不贴合真实轮廓。如果训练时喂硬标签0/1模型会被逼着去逼近那个错误边界推理结果就会出现把背景吞进来或者把前景切掉的现象。我一般对GT做一步高斯模糊# 对GT边界做软化sigmaX1.5是常用经验值 soft_gt cv2.GaussianBlur(gt.astype(np.float32), (0, 0), sigmaX1.5)说明这一步把边界处变成0到1的渐变让模型输出的概率在边缘处有缓冲。推理时再对概率做阈值或CRF比直接学硬边界更稳。sigmaX1.5是经验值太大边界拖沓太小没效果。配合这个技巧推理阶段的边缘锯齿会明显减少。3.4 P模式png读取的坑一个断言排查半小时前面提到cv2读P模式png会翻车这里补充一个更隐蔽的版本。有些数据集发布时对GT做了调色板重映射像素值不是VOC标准的0~20而是从1开始计数的mask id。如果训练脚本里不检查loss会一直不收敛看起来像模型问题实际是标签空间错了。# 校验GT是否符合预期 assert set(np.unique(gt)).issubset({0, 15}), funexpected labels: {np.unique(gt)}它的作用是一开始就暴露标签映射错误而不是等训练三个小时后才发现loss异常。这类问题在换数据集时最容易发生。特别是从网上下载的zip包解压后目录结构混乱拿错GT版本的情况很常见断言是成本最低的保险。4. 训练与推理的最小可复现流程模型改两行参数照这组抄这一章给一份可以直接跑的食谱。模型用torchvision的预训练deeplabv3_resnet101数据用上一章处理好的二分类GT训练30到50个epoch。下面是最小训练脚本和推理脚本参数给了推荐值也标了哪些是雷区。4.1 最小训练脚本替换分类头、加权CE和poly学习率# 基于torchvision的deeplabv3_resnet101最小微调脚本关键部分 import torch from torch import nn from torchvision.models.segmentation import ( deeplabv3_resnet101, DeepLabV3_ResNet101_Weights, ) model deeplabv3_resnet101( weightsDeepLabV3_ResNet101_Weights.COCO_WITH_VOC_LABELS_V1 ) # 替换最后一层分类器21类 - 2类 model.classifier[4] nn.Conv2d(256, 2, kernel_size1) model.aux_classifier[4] nn.Conv2d(256, 2, kernel_size1) # 背景占多数给背景更小的loss权重 class_weight torch.tensor([0.2, 1.0]) criterion nn.CrossEntropyLoss(weightclass_weight) optimizer torch.optim.SGD( model.parameters(), lr0.007, momentum0.9, weight_decay1e-4 ) def poly_lr(epoch, max_epoch, base_lr0.007, power0.9): return base_lr * (1 - epoch / max_epoch) ** power逻辑说明model.classifier[4]和model.aux_classifier[4]分别是DeepLabHead和辅助头里最后一个1x1卷积层只改这一层前面所有预训练参数不动。分类头从21类改成2类对应background和person。class_weight0.2:1.0是压制背景权重——VOC/COCO里背景像素占比常超过90%不加权的话模型全预测背景loss也不高但人物区域全丢。optimizer用SGDmomentum而不是AdamAdam在分割任务上收敛快但最终精度略低且对lr敏感SGD配合poly衰减是DeepLab系列里最稳的组合。4.2 参数表crop、batch、lr、epoch怎么配才算稳参数推荐值说明crop size512x512与OS16匹配太大显存爆太小大人物被截断batch size816G显存/ 48G显存显存不够时batch减半lr也要相应减半初始lr0.007SGD冻结backbone微调头时用0.007解冻后降到0.001epoch30~50几千张图30够混合数据集建议50aux_lossTruetorchvision默认开aux分支的loss按0.4权重叠加学习率策略poly(power0.9)每步衰减比固定lr和step衰减更稳这些参数是按ResNet101OS16给的。如果换MobileNetV2lr可以放大到0.01因为模型小、收敛更快。换batch时记住一个关系batch减半lr最好也减半否则梯度噪声变大loss会抖。训练时把验证集的person IoU打出来这个指标比总loss更能反映抠图质量。4.3 推理脚本短边等比例缩放输出alpha图训练完的模型推理很简单但有个细节比模型本身更影响结果输入图片的resize方式。很多人直接把任意尺寸图resize成512x512正方形人物会被压胖或压瘦分割质量肉眼可见下降。我一般按短边等比例缩放到512然后直接喂给模型因为deeplabv3会把输出上采样回输入分辨率并不要求输入是固定方形。# 推理读图 - 短边缩放 - softmax - alpha图 import torch from PIL import Image import torchvision.transforms.functional as F model.eval() img Image.open(input.jpg).convert(RGB) w, h img.size scale 512 / min(w, h) img img.resize((round(w * scale), round(h * scale))) x F.to_tensor(img) # [0,1]归一化CHW x F.normalize(x, mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) x x.unsqueeze(0).cuda() with torch.no_grad(): out model(x)[out] # (1,2,H,W)已是输入尺寸 prob torch.softmax(out, dim1)[0, 1] # person通道概率 alpha (prob 0.5).float().cpu().numpy() * 255 Image.fromarray(alpha.astype(uint8)).save(alpha.png)逻辑说明F.to_tensor把PIL图转成CHW且值归一化到0~1normalize的mean/std是ImageNet统计值预训练模型是在这个分布上训的不归一化直接喂会导致概率偏移。prob取的是通道1即person类。alphaprob0.5得到硬mask先看整体效果边缘精修留给第6章的后处理。deeplabv3是全卷积网络理论上可以吃任意分辨率但太大比如4K会让显存爆掉。短边512是安全和速度的折中如果原图特别大可以先缩到短边1024推理再用CRF把细节捞回来。4.4 显存和速度不够时怎么办部署阶段如果GPU显存只有4G或者要跑视频流有两个方向。半精度推理model.half()同时x.half()显存几乎减半速度有提升前提是显卡支持FP16加速Pascal以后的架构基本都行。换backbone把deeplabv3_resnet101换成deeplabv3_mobilenet_v3_large或deeplabv3_mobilenetv2代码只改模型名前面所有输入输出接口不变。MobileNet系列在低显存设备上是唯一能跑实时的地方。显存实在不够还有一种方法把大图切成512x512的patch逐patch推理再拼回去。注意拼接处至少要重叠32像素否则接缝处会出现一条明显的语义断裂。重叠区域取两次推理的平均值能减轻痕迹但整体耗时上升明显只适合单张大图精修不适合批量。5. 避坑指南边缘锯齿、漏检、loss震荡和zip伪加密这一章写的都是实际跑项目时遇到的故障每条按现象、原因、解决展开可以直接对着排查。5.1 边缘像楼梯GT粗糙和硬阈值是元凶现象推理出来的alpha图边缘呈锯齿状人物的头发丝被一块块切掉放大看像楼梯。原因一方面是VOC这类数据集的GT边缘本来就不贴发丝另一方面推理时用0.5硬阈值把概率从0.49到0.51的渐变像素一刀切成0或255楼梯感被放大。解决训练阶段对GT做边界软化推理阶段改用CRF或者羽化。我一般先跑CRF如果只是要快速出图直接把prob0.5改成prob0.4再叠加一次半径1像素的中值滤波楼梯感会明显减少。要说明的是中值滤波会损失一点细节但比锯齿容易接受。5.2 漏检和误检类别不均衡的连锁反应现象一张多人合照模型只输出两个人或者背景里穿裙子的人被当成背景。原因训练集里person像素占比远低于背景加权交叉熵的权重没调好。还有一个隐藏原因crop size 512会把大尺寸原图里的小人物切掉——缩放后人物只有20像素高基本被当噪声。解决把背景loss权重从0.2进一步降到0.1观察person的IoU是否上升数据增强里加0.5~1.5的随机缩放让模型见过更多尺度的人如果小目标还是漏把crop size提到640配合batch降到4。提示class_weight的基准是背景像素占比。如果训练集统计出person像素占比只有3%背景权重取0.03~0.1之间然后观察验证集person IoU这个指标比总loss更能反映抠图质量。5.3 loss震荡不收敛先查lr和BN别急着调模型现象前三个epoch loss在降第四个epoch开始突然跳到初始值然后来回震荡。原因解冻backbone时学习率还是0.007对ImageNet预训练权重来说太大了一步就把权重推出有利区域。另一个常见原因是batch size太小比如2BN的running stats在小batch上不稳定。解决解冻backbone时把lr降到0.001batch小于4就用syncBN多卡或者把BN换成GroupNorm还有一个后悔药加载上一次保存的checkpoint把lr调低再继续不要重新训。确认BN问题的方法是打印每个batch的loss如果loss在每个step之间像噪声一样跳而epoch均值没有下降趋势基本就是lr或BN的锅。5.4 数据集zip伪加密Windows解压失败的修复现象从网盘下载的数据集zipWindows右键“全部解压缩”弹出需要密码或者解到一半报“不可预料的压缩文件末端”但文件明明没有加密。原因打包工具在zip的general purpose bit里置了0x0001加密标志位但数据本体没有真正加密这叫伪加密。Windows自带解压对这一位很严格发现标志位就要求密码部分网盘转存还会把文件尾注释截断导致“末端”报错。解决优先用7-Zip它对伪加密的容忍度高右键“解压到当前文件夹”基本能直接绕过。命令行下用7z解压7z x dataset.zip -odataset -y如果7-Zip也报错先看文件大小和分卷编号是不是有多卷没下载全确认单卷后可以用Python把伪加密标志位清掉再交给Windows解压。伪加密不是真加密不需要移密码把标志位清零就是正常zip。网上搜“zip密码移除”大部分案例其实是伪加密的误报真正加密的文件在7-Zip里会要求输入密码这两个场景要分清。5.5 推理太慢CPU跑ResNet101是自找苦吃现象一张512的图在CPU上推理要30秒视频流完全没法用。原因ResNet101的算力需求在那里CPU推理没有优化指令集就是慢这不是代码问题改Python代码解决不了。解决能上GPU就上GPU哪怕老一点的GTX 1060也比CPU快一个数量级。没有GPU的场合换MobileNetV2单张CPU推理能压到5秒以内再用ONNX Runtime加int8量化还能再压一半。说到底deeplabv3的实时部署不是模型问题是硬件选型问题别在算法层面空耗。6. 把粗mask变成能交付的抠图CRF、羽化和批量脚本6.1 CRF把边缘收进真实轮廓CRF条件随机场是分割后处理的老牌选手原理是把“像素颜色相近的邻居倾向于同一类”作为约束迭代优化概率图。对deeplabv3的输出CRF最明显的改善是让边缘贴合真实轮廓。pydensecrf库是老库Python 3.8以上经常编译不过建议直接conda装或者用源码编译。# CRF细化边缘pydensecrf安装请用python 3.7以下或源码编译 import numpy as np import pydensecrf.densecrf as dcrf from pydensecrf.utils import unary_from_softmax def crf_refine(img, prob, compat3, sxy5, srgb1, iters5): H, W prob.shape unary unary_from_softmax( np.stack([1 - prob, prob], axis0).reshape(2, H * W) ) d dcrf.DenseCRF2D(W, H, 2) d.setUnaryEnergy(unary) d.addPairwiseGaussian(sxysxy, compatcompat) d.addPairwiseBilateral(sxysxy, srgbsrgb, rgbimgnp.ascontiguousarray(img)) Q d.inference(iters) return Q[1].reshape(H, W) # person类概率逻辑说明unary_from_softmax把模型输出的两类概率转成CRF的unary能量pairwise gaussian负责空间连续性sxy5表示5个像素内的邻居互相影响bilateral额外引入颜色相似度srgb1是颜色通道的标准差值越小颜色越敏感。参数经验人物抠图用compat3、sxy5、srgb1不会把边缘磨没。iters取5就够了再大只增加耗时。6.2 羽化代替硬阈值alpha过渡更自然CRF输出仍然是概率如果你要的是带半透明过渡的alpha图比如头发丝就不要做0/1硬阈值而是对概率做一个sigmoid拉伸。模型在发丝处输出的概率往往是0.3~0.7的渐变值直接截断会丢失这些半透明信息。# 概率图 - 8bit alpha保留过渡 alpha (prob * 255).astype(uint8) # 如果对比度不够用sigmoid拉伸 import numpy as np alpha 255 / (1 np.exp(-10 * (prob - 0.5)))说明sigmoid相当于把0.5附近的概率拉开让中间过渡更集中但又没完全截断。参数10是锐化系数数值越大过渡越窄越小越柔和。实际交付时看用途做电商白底图习惯硬边做合成特效习惯软边我的默认做法是两种都导出一份让下游自己选。6.3 批量抠图的脚本骨架项目落地最后一步往往是批量处理。写一个目录遍历脚本把前面的推理和后处理串起来输出原图尺寸的alpha图和合成图# 批量抠图遍历input_dir输出alpha和合成图 import os from PIL import Image for name in os.listdir(input_dir): img Image.open(os.path.join(input_dir, name)).convert(RGB) prob run_model(img) # 前面定义的推理函数 alpha (prob * 255).astype(uint8) # 合成到白底 white Image.new(RGB, img.size, (255, 255, 255)) comp Image.composite(img, white, Image.fromarray(alpha)) comp.save(os.path.join(output_dir, name))注意点合成前要确认alpha尺寸和原图一致deeplabv3输出和输入同分辨率所以推理函数里不要随意resize掉原图尺寸。批量处理时每处理一张就即时写盘不要攒到最后一次性存否则中途崩一次全部白跑。我现在接到临时抠图需求第一反应还是先用deeplabv3出一个人物概率图再根据用途决定走CRF还是直接羽化。这套流程跑顺之后1000张图的批量抠图大概一顿午饭时间就能跑完边缘质量比手动抠稳定。要提醒的是模型方案不是银弹遇到婚纱、薄纱这类半透明服饰语义分割天然会把它当半透明背景吞掉这时候老老实实改用专门的matting方案会更快。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →