ARTICLE DETAIL

资讯详情

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

大型宠物图像语义分割数据集实战:从标签转换到U-Net训练全流程

大型宠物图像语义分割数据集实战:从标签转换到U-Net训练全流程 简介大型宠物图像语义分割数据集共约七千张图像与对应掩码面向语义分割入门与进阶研究者提供背景、宠物、边缘三类像素级标签可直接用于分割模型训练与效果评估。压缩包共两千个文件其中PNG图片1998个另含类别说明TXT与可视化PY脚本总容量约766MB数据已按约5100张训练集、2200张验证集划分图像与掩码一一对应免去自行划分的麻烦。随包附带的可视化脚本可随机抽取一张图片同屏展示原始图、GT图以及GT在原图上的蒙板效果并保存到当前目录方便快速核对标注质量。结合作者关于医学图像分割网络及UNet、SwinUNet等改进方法的专栏内容可进一步把握该数据集在不同网络结构下的使用思路。当前已有四十八人学习/下载。1. 大型宠物图像语义分割数据集约7000张数据标签值不值得用先看这几点想做宠物图像分割最麻烦的往往不是搭 U-Net而是手里没有一批干净、能直接开训的数据。大型宠物图像语义分割数据集约7000张数据和标签就是为这个场景准备的原图和像素级标注一一对应拿来训练语义分割模型、做迁移学习、跑分割算法对比都合适省去自己标注和清洗的体力活。它对三类人最有用消化不了 COCO 那种庞大数据集的个人开发者需要固定类别做可控实验的研究生以及在 POC 阶段想用最少时间验证宠物分割效果的工程师。本文直接拆解这个数据集怎么验证、怎么转成训练格式、怎么跑通最小训练闭环再把我在标签格式、数据增强、类别不均衡上踩过的坑和参数设定一并写出来。2. 先摸清这个 7000 张分割数据集的底细目录结构、标签定义与自动校验2.1 语义分割数据集的常见组织方式与标签形态拿到这种大型宠物图像语义分割数据集最常见组织方式是data/下分images/和labels/两个目录文件名一一对应。标签可能是三通道 RGB 彩色 PNG相同颜色代表同一类别也可能是单通道索引 PNG像素值直接就是类别 ID资源站下载时两种都可能碰到。宠物场景一般就背景、宠物整体、再细分眼睛/耳朵/鼻子等几个类别背景和前景的颜色定义一定要在训练前确认否则后面转换索引图时会出现整个类别丢失。之所以要先摸清底细是因为语义分割的标签形态和分类、检测完全不同。分类只需一个整数检测给几个框坐标分割则要求每个像素都带上类别编号。这里的“约 7000 张数据和标签”指的是 7000 张原图配 7000 张掩码你用 PyTorch 的 Dataset 读取时必须明确标签的通道数、颜色定义、存储格式这三点直接影响预处理管线怎么写。多数人翻车不是模型没选对而是在这一步用错了读取方式。2.2 用 Python 脚本自动盘点图像与标签对应关系我拿到任何分割数据集的第一件事不是打开图片一张张看而是写一个一次性脚本盘点三类基本盘文件数量对不对得上、图像分辨率分布如何、标签里到底出现哪些颜色。宠物照片的原始分辨率跨度很大手机拍的从 512 到 4000 像素都有这决定了后续统一 resize 成多大比较合适。import os from collections import Counter, defaultdict from PIL import Image root data/pet_seg imgs sorted(os.listdir(os.path.join(root, images))) labels sorted(os.listdir(os.path.join(root, labels))) print(images:, len(imgs), labels:, len(labels)) assert [os.path.splitext(i)[0] for i in imgs] [os.path.splitext(l)[0] for l in labels], 文件名不配对 resolution Counter() color_map defaultdict(list) for i, (im, seg) in enumerate(zip(imgs, labels)): img Image.open(os.path.join(root, images, im)) mask Image.open(os.path.join(root, labels, seg)) resolution[img.size] 1 m mask.convert(RGB) colors set(m.getdata()) # 先全量去重再取 top color_map[tuple(sorted(colors))].append(im) if i 20: print(sample:, im, sorted(colors)[:10])这段代码先把文件名按字典序对齐读错掩码是最低级的错误但最容易发生然后统计分辨率分布语义分割训练时会据此决定输入尺寸比如多数图接近 800×600 就设 512接近 2000×2000 的就考虑 padding 裁剪而不是整图缩放最后统计标签里出现的颜色组合如果不同图片的颜色映射不一致训练前必须统一。convert(RGB)是为了覆盖 P 模式存储的标签——很多标注工具导出时会把调色板写进 PNG 的 P 通道直接getdata()拿到的不是真实视觉颜色。2.3 先确认类别定义再谈训练这个数据集里标签的类别定义常见为背景0、宠物整体1细分时眼睛/鼻子/皮毛各占一个 ID。用上面脚本打印出某张图的 top 颜色列表基本就能确定类别数。如果看到 0/255 两极分布说明标签可能是“每类单存一张二值图”而不是一张多类别彩色索引图这两种设计差异很大后者可以直接转 ID前者要多做一次叠加合并。提示拿到数据集后最值得做的第一件事不是跑模型而是把 20~30 张“原图 标签叠加图”并排导出成一张大图肉眼检查标签边缘和类别定义。这一步能省掉后面大半调试时间。我自己的习惯是额外写一个列表把每个类别的像素占比打印出来。宠物毛发这类类别往往占比很大眼睛/鼻子占比可能不到 1%这直接决定后面要不要用加权损失。另外也要看一眼是否存在“整张图都是背景”的标签这种样本在 7000 张里可能混入 10~20 张留着只会扰乱 BatchNorm 统计量。2.4 别把目标检测的数据集划分思路照搬过来搜“yolov8 训练自己的数据集”时能看到很多目标检测的划分教程但要明确语义分割数据集不建议直接按目标检测的习惯全部 resize 到固定正方形再做划分因为分割任务对物体的形状细节更敏感正方形缩放会把猫狗拉变形。常见做法是固定长边缩放 随机裁剪或直接按分辨率中位数设一个输入尺寸。划分前也不要把原图复制一遍做缓存7000 张原图 标签全部复制大概是几个 GB完全可以用软链或路径列表代替。3. 把彩色标签转成 class-id 索引转换脚本与四个边界坑3.1 为什么不能直接把彩色 PNG 当训练标签U-Net 这类语义分割模型训练时要求 mask 是一个(H, W)的单通道长整型张量像素值就是类别 ID。彩色 PNG 是(H, W, 3)形状把它直接送进 CrossEntropyLoss 会报维度错误或者被模型当成三通道特征图输出维度对不上loss 来回震荡。所以固定第一步建立“颜色 → 类别 ID”的对照字典把所有标签转成单通道索引图。边界坑在这里宠物毛发的边缘存在大量抗锯齿像素颜色不是纯色直接mask 颜色会把边缘像素丢成背景造成轮廓学不清。常见做法是先用第 2 章的脚本把所有颜色打出来写全映射字典遇到只差几个灰度值的中间色就归到最近的主色上。我的选择是保留较全的颜色映射而不是压缩成 0/255 二值图。3.2 转换成单通道索引并同步划分 train/valimport numpy as np from PIL import Image import os # 颜色到类别的映射按你自己的数据集调整 COLOR2ID { (0, 0, 0): 0, # 背景 (255, 255, 255): 1, # 宠物整体 (255, 0, 0): 2, # 眼睛 (0, 255, 0): 3 # 鼻子 } def rgb_to_id(mask_rgb: np.ndarray) - np.ndarray: h, w, _ mask_rgb.shape idx np.zeros((h, w), dtypenp.uint8) for rgb, cls_id in COLOR2ID.items(): match (mask_rgb np.array(rgb)).all(axis-1) idx[match] cls_id return idx os.makedirs(data/pet_seg/labels_id, exist_okTrue) for f in sorted(os.listdir(data/pet_seg/labels)): mask Image.open(fdata/pet_seg/labels/{f}).convert(RGB) arr np.asarray(mask) idx rgb_to_id(arr) Image.fromarray(idx, modeL).save(fdata/pet_seg/labels_id/{os.path.splitext(f)[0]}.png)逻辑说明COLOR2ID字典来自第 2 步统计到的实际颜色rgb_to_id先建全零背景再将每个类别颜色对应区域填充为 ID避免把背景写成 255 的常见错误最后以modeL单通道灰度图保存PNG 无损且能直接被 PIL 按灰度读回不会引入 JPEG 压缩噪点。参数说明类别 ID 从 0 开始符合多数损失函数的默认设定如果有 ignore 区域可以预留一个 ID 如 255在训练损失里配合ignore_index255使用。3.3 划分数据时固定随机种子并按类别分布检查import random, json, glob random.seed(2025) pairs sorted(glob.glob(data/pet_seg/labels_id/*.png)) random.shuffle(pairs) n len(pairs) val pairs[: int(n * 0.1)] test pairs[int(n * 0.1): int(n * 0.2)] train pairs[int(n * 0.2):] with open(data/pet_seg/split.json, w) as f: json.dump({train: train, val: val, test: test}, f, indent2)逻辑说明random.seed(2025)让每次运行产生的划分一致语义分割实验对验证集稳定性敏感不能每次训练换一批验证图train/val/test 按 80/10/10 切分7000 张量级对应 5600/700/700。划分后最好对三个子集分别统计各类别像素占比确认没有出现某个类别只在训练集出现的情况。如果发现偏差就换用分层采样先按类别存在性把图片分桶再在各桶内随机分配。参数说明10% 验证集对语义分割来说是比较低的下限再低 mIoU 波动会很大但超过 20% 又会明显减少训练数据量。如果你的显存允许更大 batch可以保持这个划分比例不变只调输入尺寸。3.4 四个边界坑汇总坑现象原因解决索引坑loss 极高且不降标签是 RGB 但当成单通道读先转成modeL索引图模式坑标签显示为灰色乱码PNG 以 P 模式存储调色板信息丢失用convert(RGB)再映射划分坑某类别在验证集消失直接随机划分未做分层按类别桶分层采样漏类坑错例全是边缘像素抗锯齿中间色未映射用“最近主色”补齐颜色表这四条是转换阶段最常踩的全部规避后预处理管线才算稳定。4. 跑通 U-Net 图像分割训练闭环PyTorch 最小实现与参数4.1 自定义 Dataset 读取图像和单通道 mask训练前先把数据读取写好。这个 Dataset 干三件事按 split.json 的路径读图、读单通道 mask、同步缩放到固定尺寸。关键点在于图像和掩码的缩放方式必须不同图像用双线性掩码用最近邻。import torch from torch.utils.data import Dataset from PIL import Image import numpy as np, json class PetSegDataset(Dataset): def __init__(self, split_path, splittrain, size(512, 512)): with open(split_path) as f: self.paths json.load(f)[split] self.size size self.mean torch.tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1) self.std torch.tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1) def __getitem__(self, i): label_path self.paths[i] img_path label_path.replace(labels_id, images).replace(.png, .jpg) img Image.open(img_path).convert(RGB) mask Image.open(label_path) img img.resize(self.size) mask mask.resize(self.size, resampleImage.NEAREST) x torch.from_numpy(np.asarray(img)).permute(2, 0, 1).float() / 255.0 x ((x - self.mean) / self.std).squeeze(0) y torch.tensor(np.asarray(mask), dtypetorch.long) return x, y def __len__(self): return len(self.paths)逻辑说明label_path指向第 3 章生成的labels_id目录replace操作把标签路径反转回图像路径前提是两目录文件名保持一一对应。掩码 resize 用Image.NEAREST这一步如果误用双线性插值会在类别边界产生中间值直接污染损失计算。归一化用的 ImageNet 统计量是通用做法不是必须但在迁移学习场景下能明显加速收敛具体参数是mean(0.485,0.456,0.406)、std(0.229,0.224,0.225)。参数说明size(512, 512)是显存和精度之间的折衷。8GB 显卡跑完整 U-Net 配 batch8512 输入比较紧张建议改 384 或 256如果显存 16GB 以上可以上 512 并配合 batch8。不要一开始就追求 768宠物数据里物体边界细节是重要信息但输入太大训练迭代太慢先用 512 跑通闭环最重要。4.2 最小 U-Net 结构与损失函数选择语义分割模型的选型理由很简单这个 7000 张量级的数据集不需要一上来就上大模型Mini U-Net 足够跑通和验证数据质量。U-Net 的对称编码-解码结构对宠物这种“大目标 中等边缘复杂度”的场景很合适参数少改起来快。import torch import torch.nn as nn def double_conv(in_ch, out_ch): return 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), ) class MiniUNet(nn.Module): def __init__(self, in_ch3, num_classes4): super().__init__() self.enc1 double_conv(in_ch, 64) self.pool nn.MaxPool2d(2) self.enc2 double_conv(64, 128) self.enc3 double_conv(128, 256) self.enc4 double_conv(256, 512) self.up nn.Upsample(scale_factor2, modebilinear, align_cornersFalse) self.dec3 double_conv(768, 256) self.dec2 double_conv(384, 128) self.dec1 double_conv(192, 64) self.out nn.Conv2d(64, num_classes, 1) def forward(self, x): e1 self.enc1(x) e2 self.enc2(self.pool(e1)) e3 self.enc3(self.pool(e2)) e4 self.enc4(self.pool(e3)) d3 torch.cat([self.up(e4), e3], dim1) d3 self.dec3(d3) d2 torch.cat([self.up(d3), e2], dim1) d2 self.dec2(d2) d1 torch.cat([self.up(d2), e1], dim1) d1 self.dec1(d1) return self.out(d1) criterion nn.CrossEntropyLoss(ignore_index255)逻辑说明double_conv是 U-Net 里最基础的卷积块两个 3×3 卷积加 BN 加 ReLU主分支采用torch.cat做通道拼接而不是相加语义分割中跳跃连接保留的是编码器的高分辨率特征拼接可以让解码器同时看到低层边缘信息和高层语义信息。输出层是一个 1×1 卷积把 64 通道映射到类别数没有接 softmax因为 PyTorch 的 CrossEntropyLoss 会内部处理。参数说明num_classes4对应第 3 章的背景/宠物/眼睛/鼻子四类ignore_index255是给可能有标注空洞的区域预留的如果验证时发现边缘纹理不清可以考虑叠加 Dice Loss但先期只跑 CrossEntropy 更容易判断数据集标签本身是否干净。4.3 训练循环与关键参数设定from torch.utils.data import DataLoader model MiniUNet(num_classes4).cuda() opt torch.optim.AdamW(model.parameters(), lr3e-4, weight_decay1e-4) sched torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max40) loader DataLoader(PetSegDataset(data/pet_seg/split.json, train), batch_size8, shuffleTrue, num_workers4, pin_memoryTrue) for epoch in range(40): model.train() run_loss 0.0 for x, y in loader: x, y x.cuda(), y.cuda() pred model(x) loss criterion(pred, y) opt.zero_grad() loss.backward() opt.step() run_loss loss.item() sched.step() torch.save(model.state_dict(), fpet_unet_epoch{epoch}.ckpt) print(epoch, loss, run_loss / len(loader))逻辑说明训练循环比较朴素但已经包含几个容易出错的细节。model.train()必须显式调用否则 BatchNorm 的 running stats 不更新torch.save每 epoch 存一次是防半路 OOM 或显存被占用导致的训练中断权重文件不大存了才有后悔药。参数说明lr3e-4配 AdamW 是稳定起点数据集只有 7000 张学习率过高容易在最初几个 epoch 就把特征破坏掉weight_decay1e-4用于抑制过拟合T_max40表示 40 个 epoch 内余弦退火降低到接近 0。batch_size8在 512 输入 8GB 显存下比较合适如果 OOM 就把 batch 降到 4 或把输入尺寸改 384不要先改模型结构。4.4 用验证集计算 mIoU 防自欺def evaluate(model, loader, num_classes4): model.eval() miou 0.0 with torch.no_grad(): for x, y in loader: pred model(x.cuda()).argmax(1).cpu() inter 0 union 0 for cls in range(num_classes): pred_c (pred cls) y_c (y cls) inter (pred_c y_c).sum().item() union (pred_c | y_c).sum().item() miou inter / max(union, 1) return miou / len(loader)逻辑说明这个evaluate用的是逐类交并比累加的平均值比像素精度更能反映细粒度类别真实水平。语义分割里背景往往占 80% 以上模型如果全部预测成背景像素精度也能达到 80%mIoU 一下子就能揭穿这种自欺。实际跑时建议在每个 epoch 末尾打印一次 val mIoU并同步保存best模型。这里参数num_classes要与模型输出层一致max(union, 1)防止除零。5. 宠物分割数据集训练避坑标签错位、进程翻车、类别不平衡的五条排查记录5.1 标签颜色是 255/0 而非类别索引导致损失不下降现象loss 卡在 2.0 以上死活不降连续 20 个 epoch 输出全是背景。原因标签 PNG 虽然是彩色但标注工具是“每个类别单独存成一张二值图”读取后被当成三通道 RGB背景像素 0/0/0 与类别像素 255/255/255 混在一起模型没法学到有意义的类别分布。解决回到第 2 章脚本打印每个标签的像素值集合。如果只有 0 和 255 而没有任何中间色就说明是“二值图集合”不是“单张索引图”需要把多张二值图叠加成一张索引图叠加规则是类别 ID 从 1 开始逐层覆盖。这个坑在资源站下载的数据集里出现概率极高先花五分钟检查省下三小时训练。5.2 数据增强时图像翻转了但掩码没翻现象训练 loss 越来越低验证 mIoU 却停滞把验证集预测图叠在原图上看到宠物轮廓是镜像翻转错位的。原因常见的albumentations或torchvision.transforms写法里很容易只给图像做了随机翻转掩码没有同步。分割任务要求增强操作对图像和掩码“同时、同参数”施加。解决用albumentations.Compose时把 mask 一起传入手写变换就固定一个随机数 seed先对图像做变换记录参数再对掩码施加同样参数。最简单的方案是 Dataset 里只做RandomHorizontalFlip传入img和mask两个参数不搞复杂的随机裁剪。这一步踩到的概率仅次于标签格式问题几乎是每个做语义分割的新手必修课。5.3 7000 张里混入坏图和全背景图现象DataLoader 跑到一半突然报OSError: image file is truncated或者某张图读出来是全黑的。原因网络下载的数据集往往会有几张国损坏或标注不完整的图。全背景图影响更隐蔽——它不报错但梯度几乎为零浪费训练时间还拉低了验证集 mIoU 的可信度。解决在预处理阶段做一轮完整性扫描用PIL.Image.open逐张打开并load()读失败或getdata()后全是 0 的图记录到broken.txt从数据集中剔除。这个扫描大概几分钟属于一次性成本。不要试图在训练循环里做容错异常分支会拖慢 DataLoader 且难以排查。5.4 类别极不均衡mIoU 虚高但细粒度类别全废现象猫和狗占画面 80%眼睛这一类的 IoU 只有 2%总体 mIoU 却跑到 75% 以上看起来效果不错。原因所有类别的 IoU 取平均时占比大的类别权重高细粒度类别贡献被淹没。宠物场景里眼睛、鼻子这类小目标本来像素占比就低天然容易被背景类带偏。解决记录每个类别的 IoU 而不是只记平均在损失层面可以引入类别权重CrossEntropyLoss(weightclass_weight)权重按“1/像素占比”粗略归一化即可更实用的是对占比最小的类别做 Oversample比如眼中等小目标样本如果只有 100 张就在每个 epoch 里重复采样 3 次。我用下来偏向后者因为类别权重调不好会把背景学坏。5.5 显存占用过高和数据加载卡顿现象batch_size8 直接 OOMCPU 内存涨到 64GB 仍不够训练速度几乎停滞。原因Dataset.__getitem__每次读原始大图再做 resize7000 张图如果全是 3000 像素长边每张读入内存就要几十 MB8 个 worker 并行时峰值内存直接爆掉。U-Net 的显存占用大头在编码器第一层输入尺寸微调对显存影响非常明显。解决先做一次“预缩放缓存”把所有图和标签统一 resize 到 512 或 384保存到新目录训练时直接读缩放后的图缓存目录可以用torch.load或npz存储SSD 上读取速度比 readme 里的大图快很多。还是OOM的话把num_workers降到 2、pin_memory关掉检查是否 CPU 内存被多进程吃满。我遇到过一次玄学卡顿最后发现是num_workers16导致每个进程都在读原图改成 4 后流畅了。6. 迁移学习与多尺度推理让约 7000 张数据发挥真正价值6.1 用预训练编码器替代练手版 U-NetMini U-Net 跑通后想提升精度就需要迁移学习。常见做法是直接用 torchvision 自带的 DeepLabV3-ResNet50 预训练权重冻结 backbone 训分类头再解冻全量微调。这个操作对 7000 张数据尤其合适预训练编码器已经能识别纹理和形状只需要把宠物类别映射到新数据上收敛快且不容易过拟合。import torchvision model torchvision.models.segmentation.deeplabv3_resnet50(weightsDEFAULT) model.classifier[4] torch.nn.Conv2d(256, 4, kernel_size1) for p in model.backbone.parameters(): p.requires_grad False逻辑说明classifier[4]是 DeepLabV3 最后的 1×1 卷积输出层类别数从预训练的 21 改成 4冻结 backbone 后前 10 个 epoch 只更新分类头后续再解冻全部参数以 1e-4 学习率微调。这样做我在宠物分割上要比从头训练 Mini U-Net 高出 10 个点左右而且训练时更稳定。参数说明weightsDEFAULT在 torchvision 0.13 以上可用冻结阶段学习率仍用3e-4即可解冻后降到1e-4。6.2 多尺度推理与可视化验证宠物在画面中的尺度变化大单尺度推理容易漏掉小猫的轮廓。推理时把图缩放成 512 和 640 两个尺寸分别预测将概率图双线性插值回原尺寸再取平均最后 argmax。这个技巧不用改训练代码只在验证脚本里加几行。更重要的还是可视化把预测 mask 描边叠到原图上每个样本存一张对比图错例按类别归档。我自己的习惯是每跑完一轮实验把 20 张“预测错得最离谱”的图单独看检查是标签错还是模型边界不清。发现模型把猫耳朵边缘学成毛刺时我会回到第 3 章颜色映射检查抗锯齿像素而不是调模型结构。约 7000 张的规模其实很尴尬不够撑起大数据集训练幻想但足够做一次可信的验证先跑通基线、再上预训练、最后用 mIoU 和错例图说话。希望帮到你。本文还有配套的精品资源点击获取
返回列表