简介本资源面向计算机视觉初学者与语义分割实践者围绕 PyTorch 自带 deeplabv3 实现人物抠图这一二分类语义分割任务展开帮助读者掌握从模型调用到预测输出的完整流程。包内共 3453 个文件以 3411 个 png 图像数据为主辅以 15 个 py 脚本、20 个 pyc 编译文件以及 Dockerfile、sh 启动脚本、LICENSE、README.md 等工程配置压缩包约 990.96MB目录结构便于按数据、代码、配置分块查阅。已有 1260 人学习下载。资源覆盖 deeplabv3_resnet50、resnet101、mobilenet_v3_large 等多种骨干网络的替换测试思路并包含 wandb 训练可视化、交叉熵与 dice_loss 组合损失、二分类语义分割预测等关键实现读者可据此复现人物抠图流程理解损失设计与评估方法并在此基础上迁移到其他前景背景分割场景。1. 从一张人像照到干净 alpha 通道这套 deeplabv3 抠图包到底能干什么手里有一张人像照片背景杂乱想把人单独抠出来换背景或者做合成这是很多人都会遇到的需求。传统做法是上 Photoshop 用钢笔工具一点点描边费时费力遇到头发丝、半透明边缘基本就翻车。这套资源给了一条工程化的路子用 PyTorch 自带的 deeplabv3 做二分类语义分割把「人」和「背景」分开输出一张前景 mask再合成到纯色或透明背景上得到类似抠图的效果。包里带了 Dockerfile、README、若干测试图00315.png、00458.png 等和一张 output.jpg 示例结果说明作者已经把环境和推理链路跑通过一遍。它适合两类人一是想快速验证 deeplabv3 在自己图片上效果的算法工程师二是想学二分类语义分割完整流程数据、损失、训练、预测、可视化的学生或转行者。需要提前说清楚deeplabv3 做的是语义分割不是实例分割多人场景下它会把所有人当成同一类不会区分个体这是选型时必须知道的边界。2. deeplabv3 二分类分割的原理与模型选型为什么是它而不是 U-Net2.1 空洞卷积与 ASPP 到底解决了什么问题deeplabv3 的核心卖点是两个东西空洞卷积Atrous Convolution和 ASPPAtrous Spatial Pyramid Pooling。普通卷积下采样会丢分辨率语义分割又要求输出和输入同尺寸靠不断上采样恢复细节边缘就会糊。空洞卷积的做法是在卷积核里插空不增加参数量的前提下扩大感受野让每个像素点能「看到」更大范围的上下文。ASPP 则是在特征图后面并行挂几个不同膨胀率的空洞卷积和一个全局池化分支把多尺度信息拼起来。对人像抠图来说这个设计直接对应两个难点头发丝这种细结构需要高分辨率细节身体轮廓这种大结构需要足够大的感受野ASPP 正好两头都照顾到。PyTorch 的 torchvision 里现成提供了三个 deeplabv3 主干deeplabv3_resnet50、deeplabv3_resnet101、deeplabv3_mobilenet_v3_large。资源摘要里明确提到可以换模型测试这不是客套话。resnet50 是精度和显存的平衡点8G 显存跑 513×513 输入基本够用resnet101 精度略高但显存和耗时都上去了适合离线批量处理mobilenet_v3_large 是轻量主干速度最快适合端侧或者实时性要求高的场景但边缘细节会明显弱一些。我一般先用 resnet50 跑通全流程确认数据和损失没问题再根据部署目标换主干。2.2 用 torchvision 加载预训练 deeplabv3 的最小代码不要自己从零搭网络torchvision 的接口已经封装好了改分类数只需要动一个参数。下面这段是加载模型并替换分类头的标准写法import torch import torchvision from torchvision.models.segmentation import deeplabv3_resnet50 # num_classes2 表示二分类背景 人 # aux_lossTrue 会额外接一个辅助分类头训练时提供额外梯度推理时可忽略 model deeplabv3_resnet50( weightstorchvision.models.segmentation.DeepLabV3_ResNet50_Weights.DEFAULT, num_classes2, aux_lossTrue ) # 如果只想用主干预训练权重、分类头随机初始化可以这样控制 # 但 torchvision 的 weights 参数会同时加载分类头num_classes 不一致时自动丢弃 device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) model.eval()逻辑说明weightsDEFAULT会下载在 COCO 上预训练的权重包含主干和分类头。当num_classes2与预训练的 21 类不一致时torchvision 会自动跳过分类头权重只加载主干这是官方行为不用手动处理。aux_lossTrue在训练阶段有用推理时输出里会多一个aux键取out[out]才是主输出。参数上输入尺寸建议保持 513×513 或 520×520这是 deeplab 系列的原生训练尺度随意改成非 8 的倍数会导致下采样和上采样对不齐出现边缘错位。2.3 二分类分割的标签怎么准备语义分割的标签是一张和原图同尺寸的单通道图像素值就是类别 id。二分类场景下背景标 0人标 1。常见做法是用 labelme 或 CVAT 标注导出成灰度 PNG注意不要用 JPEG 存标签JPEG 有损压缩会把 0 和 1 的边界糊成中间值训练时直接翻车。如果手里只有人像和背景的合成数据也可以用现成的 matting 数据集如 AIM-500、P3M-10k转成二值 mask。转换时统一做一步mask (mask 127).astype(np.uint8)把任何非零值归到 1避免标注工具导出的 255 被当成第 255 类。3. 训练链路搭建交叉熵 dice_loss 组合与 wandb 可视化3.1 为什么单用交叉熵在人像分割上不够交叉熵是逐像素算的人像图里背景像素通常占 70% 以上前景占比小模型会倾向于全预测背景也能拿到不错的 loss结果就是 mask 大面积缺失。dice_loss 直接优化预测和标签的重叠度对类别不平衡不敏感但它梯度不稳定单独用容易震荡。工程上常见做法是两者加权相加交叉熵负责稳定收敛dice 负责拉高前景召回。权重一般取ce_weight1.0, dice_weight1.0如果发现前景还是偏少把 dice 权重提到 2.0 试试。import torch import torch.nn as nn import torch.nn.functional as F class DiceLoss(nn.Module): def __init__(self, smooth1.0): super().__init__() self.smooth smooth def forward(self, logits, targets): # logits: [B, 2, H, W] targets: [B, H, W] 取值 0/1 probs F.softmax(logits, dim1)[:, 1] # 取前景概率 targets targets.float() intersection (probs * targets).sum(dim(1, 2)) union probs.sum(dim(1, 2)) targets.sum(dim(1, 2)) dice (2 * intersection self.smooth) / (union self.smooth) return 1 - dice.mean() ce_loss nn.CrossEntropyLoss() dice_loss DiceLoss() def combined_loss(logits, targets, ce_w1.0, dice_w1.0): return ce_w * ce_loss(logits, targets) dice_w * dice_loss(logits, targets)逻辑说明DiceLoss里先对 logits 做 softmax 取前景通道再算 dice 系数。smooth1.0是防止前景全空时除零。combined_loss把两个 loss 加权相加训练时直接调用即可。参数上ce_w和dice_w不是固定的小目标多就加大 dice训练不稳定就加大 ce。注意 targets 必须是 long 类型给交叉熵float 类型给 dice代码里做了转换。3.2 wandb 接入把 loss 曲线和预测图一起盯住训练分割模型最怕的是 loss 降了但 mask 没变好所以光看数值不够得把中间预测图打出来。wandb 的Image和log能同时记录标量和图像配置成本很低。import wandb wandb.init(projectdeeplabv3-matting, config{ lr: 1e-4, batch_size: 4, epochs: 50, ce_weight: 1.0, dice_weight: 1.0, }) for epoch in range(epochs): model.train() for imgs, masks in train_loader: imgs, masks imgs.to(device), masks.to(device) out model(imgs)[out] loss combined_loss(out, masks) optimizer.zero_grad() loss.backward() optimizer.step() # 每个 epoch 记录一次 loss 和一张验证图 model.eval() with torch.no_grad(): val_out model(val_img.to(device))[out] pred val_out.argmax(1).squeeze().cpu().numpy() wandb.log({ train_loss: loss.item(), val_pred: wandb.Image(pred * 255, captionfepoch {epoch}) })逻辑说明wandb.init里的 config 会把超参记录下来方便对比不同实验。wandb.Image接收 numpy 数组pred * 255把 0/1 mask 转成可视灰度图。参数上project名字自己定团队协作时统一命名规则。注意 wandb 默认需要联网登录离线环境可以用wandb.init(modeoffline)日志存本地之后再同步。3.3 学习率与 batch size 的实操取值deeplabv3 微调时学习率不要开大主干预训练权重已经很好开大反而破坏特征。我一般用lr1e-4配Adam或SGD(momentum0.9)batch size 受显存限制8G 卡上 resnet50 513×513 大概能跑 batch4。如果显存不够优先降 batch 而不是降分辨率分辨率降了边缘细节损失很难补回来。可以用梯度累积模拟大 batch每 4 个 step 才optimizer.step()一次效果接近 batch16。4. 推理与后处理从 logits 到可用的抠图结果4.1 单张图推理的完整流程训练完保存权重推理时加载模型、预处理、前向、取 argmax、还原尺寸这几步顺序不能乱。下面是一个可直接用的推理函数import cv2 import numpy as np import torch import torchvision.transforms as T def predict_mask(model, img_path, device, size513): img cv2.imread(img_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) h, w img.shape[:2] transform T.Compose([ T.ToPILImage(), T.Resize((size, size)), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) inp transform(img).unsqueeze(0).to(device) model.eval() with torch.no_grad(): out model(inp)[out] pred out.argmax(1).squeeze().cpu().numpy().astype(np.uint8) # 还原到原图尺寸用最近邻避免引入中间值 pred cv2.resize(pred, (w, h), interpolationcv2.INTER_NEAREST) return pred * 255逻辑说明预处理必须和训练时一致包括归一化的 mean/std这是 ImageNet 的标准值torchvision 预训练模型都按这个来。argmax(1)在通道维取最大值得到 0/1 的类别图。还原尺寸用INTER_NEAREST双线性插值会产生 0 到 1 之间的值二值 mask 就废了。参数上size要和训练时一致训练用 513 推理也用 513不一致会导致精度下降。4.2 边缘优化把硬 mask 变成带过渡的 alphadeeplabv3 输出的是硬分割边缘是锯齿状的直接拿去做合成会有明显白边。常见做法是对 mask 做一次高斯模糊再和原图做 alpha 混合得到半透明的过渡带。如果要求更高可以用cv2.GaussianBlur加cv2.erode/dilate做形态学收边或者上 guided filter 以原图为引导做边缘对齐。这一步不是 deeplabv3 本身的功能但实际抠图项目里几乎必做否则头发区域会像剪纸。mask predict_mask(model, 00315.png, device) mask_blur cv2.GaussianBlur(mask, (7, 7), 0) alpha mask_blur.astype(np.float32) / 255.0 img cv2.imread(00315.png).astype(np.float32) fg img * alpha[..., None] bg np.ones_like(img) * 255 # 白底 composite (fg bg * (1 - alpha[..., None])).astype(np.uint8) cv2.imwrite(output_composite.jpg, composite)逻辑说明GaussianBlur的核大小控制过渡带宽度7×7 大概对应 3 像素的羽化人像一般够用。alpha是 0 到 1 的浮点前景乘 alpha背景乘 1-alpha相加就是标准 alpha 合成。参数上核大小取奇数越大边缘越软但太大主体会发虚7 到 15 之间调。4.3 批量推理与结果命名资源包里有多张测试图00315.png、00458.png、00916.png、00109.png、00396.png批量跑的时候建议保持输入输出同名方便对照。用glob遍历每张图单独走一遍predict_mask结果存到output/目录。注意批量推理时不要累积梯度torch.no_grad()一定要加否则显存会随着图片数量增长直到 OOM。5. 避坑与排查这五个问题我几乎每次都会遇到5.1 现象训练 loss 一直降但预测出来全是背景原因前景像素占比太低交叉熵主导了梯度模型学到「全预测背景」这个局部最优。解决把 dice 权重从 1.0 提到 2.0 甚至 3.0或者在采样时对前景区域做 oversampling让每个 batch 里前景比例不低于 30%。5.2 现象推理结果边缘有明显锯齿和白边原因deeplabv3 输出的是下采样再上采样的结果边界本身就不精细加上硬 argmax 没有过渡。解决按 4.2 节做高斯羽化或者把输入分辨率从 513 提到 769边缘会细一些代价是显存和耗时增加。5.3 现象换 mobilenet_v3_large 后精度掉得厉害原因轻量主干的特征表达能力弱ASPP 在多尺度融合时拿到的底层特征不够丰富。解决如果必须用轻量主干把学习率降到 5e-5训练轮数增加 20%并且冻结主干前几层只微调后面。精度要求高就别省这个算力换回 resnet50。5.4 现象wandb 记录正常但本地图片打不开原因wandb.Image接收的是 numpy 数组如果传了 torch tensor 或者值域不在 0-255wandb 会存成异常格式。解决统一转成np.uint8再传pred * 255之后加.astype(np.uint8)。5.5 现象Docker 里跑推理报 CUDA 版本不匹配原因资源包里的 Dockerfile 基础镜像 CUDA 版本和宿主机驱动不兼容。解决先nvidia-smi看驱动支持的 CUDA 上限再把 Dockerfile 里的FROM改成对应版本或者直接用--gpus all加nvidia-container-toolkit跑别在镜像里装驱动。6. 进阶技巧用 TTA 和阈值搜索把 mask 质量再抬一档基础流程跑通之后如果对精度还有要求有两个成本低、见效明显的技巧。第一个是测试时增强TTA对同一张图做水平翻转、多尺度缩放分别推理再把结果平均最后取 argmax。deeplabv3 对水平翻转比较敏感TTA 通常能带来 1 到 2 个点的 mIoU 提升。实现上就是把predict_mask包一层对每张图生成[原图, 翻转图]两个输入翻转图的输出再翻回来两张概率图相加。def predict_tta(model, img_path, device, size513): img cv2.imread(img_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) h, w img.shape[:2] transform T.Compose([ T.ToPILImage(), T.Resize((size, size)), T.ToTensor(), T.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) inp transform(img).unsqueeze(0).to(device) inp_flip torch.flip(inp, dims[3]) model.eval() with torch.no_grad(): prob F.softmax(model(inp)[out], dim1) prob_flip F.softmax(model(inp_flip)[out], dim1) prob_flip torch.flip(prob_flip, dims[3]) prob_avg (prob prob_flip) / 2 pred prob_avg.argmax(1).squeeze().cpu().numpy().astype(np.uint8) pred cv2.resize(pred, (w, h), interpolationcv2.INTER_NEAREST) return pred * 255逻辑说明torch.flip(inp, dims[3])在宽度维翻转推理后再翻回来保证空间对齐。prob_avg是两张概率图的平均比直接平均 mask 更合理因为概率包含了模型的不确定性。参数上TTA 会让推理耗时翻倍实时场景慎用离线批量处理很划算。第二个技巧是前景阈值搜索。默认argmax等价于阈值 0.5但人像抠图里 0.5 不一定最优。可以拿几张有标注的验证图把前景概率从 0.3 到 0.7 遍历算每张图的 IoU取平均最高的那个阈值。我实测下来头发多的图阈值调到 0.4 左右召回更好背景干净的图 0.5 就够。这个搜索过程写成一个循环十几行代码但比盲目调模型有效得多。best_thr, best_iou 0.5, 0 for thr in np.arange(0.3, 0.71, 0.05): ious [] for img_path, gt_path in val_pairs: prob get_foreground_prob(model, img_path, device) # 返回 0-1 概率图 pred (prob thr).astype(np.uint8) gt cv2.imread(gt_path, 0) gt (gt 127).astype(np.uint8) inter (pred gt).sum() union (pred | gt).sum() ious.append(inter / (union 1e-6)) mean_iou np.mean(ious) if mean_iou best_iou: best_iou, best_thr mean_iou, thr print(fbest thr{best_thr:.2f}, iou{best_iou:.4f})逻辑说明get_foreground_prob是前面推理函数去掉 argmax 的版本返回 softmax 后的前景通道。inter / union就是 IoU1e-6防除零。遍历完取最高 IoU 对应的阈值之后推理都用这个值替代 0.5。参数上搜索范围 0.3 到 0.7 步长 0.05 够用验证集至少 10 张图否则阈值会过拟合到某一张。这两个技巧叠加之后同一套 deeplabv3 权重出来的 mask边缘和召回都会比裸推理好一截。从那以后我每次做完分割训练都会先跑一遍 TTA 加阈值搜索再交付裸 argmax 的结果只用来快速看趋势。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?