简介:这份大型宠物图像语义分割数据集面向计算机视觉学习者与图像分割研究者,尤其适合正在实践UNet、SwinUnet、TransUnet等分割网络、需要真实多类别数据做训练与验证的人群。数据集已按训练集与验证集划分完毕,训练集约5100张图片及对应mask,验证集约2200张,共约7000张样本,涵盖背景、宠物、边缘三类像素标签,标签定义可参考classes文件。资源包共2000个文件,以1998个png图像与掩膜为主,另含1个txt类别说明和1个py可视化脚本,压缩包约765.67MB,采用7z格式。配套脚本可随机抽取一张图片,展示原图、GT图像及GT在原图上的蒙板效果并保存到当前目录,便于快速检查标注质量与数据分布。目前已有48人学习,适合用于分割模型训练、对比实验与数据预处理练习。
1. 宠物语义分割数据集:7000 张图能撑起一个可用的分割模型吗
拿到「约 7000 张宠物图像语义分割数据集」这个标题,多数人第一反应是:数据量够不够?能不能直接训出能用的模型?我先把结论摆出来——7000 张带像素级标签的宠物图像,在语义分割这个任务里属于「小而精」的规模,单类目标(比如只分猫、狗、宠物本体)完全够用,多类细粒度(区分品种、区分身体部位)就偏紧。它真正适合的人群是:想跑通语义分割全流程的算法工程师、做宠物相关产品(智能喂食器、宠物摄像头、宠物社交 App 抠图)的开发者,以及拿它当教学或预训练底座的研究者。
宠物图像有个天然难点:毛发边缘极其琐碎,猫狗的轮廓在像素级别上本身就是模糊的。这跟建筑、道路那种硬边界目标完全不同,标注时人和动物的边界怎么算、胡须算不算前景、半透明毛发怎么处理,都会直接影响标签质量。所以这个数据集的价值不在于「大」,而在于它把语义分割里最烦人的一类边界问题集中暴露出来了。你拿它练手,踩过的坑在医学图像分割、抠图、软标签这些场景里会反复遇到。7000 张的体量,单卡 24G 显存跑主流分割网络,一两天能出第一版结果,迭代成本低,这才是它最实际的意义。
2. 语义分割数据集到底长什么样:从标签格式到类别体系
2.1 语义分割和实例分割的区别,先别搞混
热词里有人问「yolo26 中实例分割与语义分割的区别」,这个问题必须先说清,否则你拿到数据集会不知道怎么用。语义分割给每个像素分配一个类别标签,同一类别的所有个体在标签图里是同一个值——三只猫全是「猫」这个类,像素值一样。实例分割则要给每只猫单独编号,猫 A 和猫 B 是不同实例。宠物数据集如果标签是单通道灰度图或调色板图,每个像素只有一个类别 ID,那就是语义分割;如果还附带每个个体的 mask 或 JSON 里的多边形实例列表,那才可能支持实例分割。
判断方法很直接:打开一张标签图,看同一类别的两只宠物是不是连成一片、值相同。是,就是语义分割。常见做法是标签存成 PNG 单通道,背景为 0,宠物为 1,或者用调色板映射多类。你拿到数据集第一件事就是确认这一点,不然训练时 loss 怎么算都是错的。
2.2 标签格式与目录结构:拿到手先做这三步检查
一个规范的语义分割数据集,目录通常长这样:
pet_seg/ ├── images/ │ ├── 0001.jpg │ ├── 0002.jpg │ └── ... ├── masks/ │ ├── 0001.png │ ├── 0002.png │ └── ... └── classes.txtimages 存原图,masks 存同名的标签图,classes.txt 写类别名和对应像素值。拿到数据集后,我一般先跑一段检查脚本,确认三件事:图像和标签是否一一对应、标签像素值是否在预期类别范围内、图像尺寸和标签尺寸是否一致。
import os import numpy as np from PIL import Image img_dir = "pet_seg/images" mask_dir = "pet_seg/masks" img_files = sorted(os.listdir(img_dir)) mask_files = sorted(os.listdir(mask_dir)) # 检查1:文件名是否一一对应 assert [os.path.splitext(f)[0] for f in img_files] == \ [os.path.splitext(f)[0] for f in mask_files], "图像与标签不匹配" # 检查2:标签像素值分布 all_vals = set() for f in mask_files[:200]: # 抽样200张足够看出类别体系 m = np.array(Image.open(os.path.join(mask_dir, f))) all_vals.update(np.unique(m).tolist()) print("标签像素值集合:", sorted(all_vals)) # 检查3:尺寸一致性 for f in img_files[:50]: stem = os.path.splitext(f)[0] img = Image.open(os.path.join(img_dir, f)) mask = Image.open(os.path.join(mask_dir, stem + ".png")) if img.size != mask.size: print("尺寸不一致:", f, img.size, mask.size)这段脚本的逻辑很直白:第一步用文件名集合比对,防止有图没标签或有标签没图;第二步抽样统计标签里出现过的所有像素值,你就能反推出类别体系——如果只有 0 和 1,是二分类(背景/宠物);如果有 0、1、2、3,可能是背景加多个宠物类别或身体部位。第三步查尺寸,语义分割要求图像和标签严格对齐,尺寸不一致的样本必须剔除或同步 resize,否则训练时对齐就错了。
参数上,抽样数量 200 和 50 是经验值,数据量小就全查。如果标签是调色板模式(P 模式),np.array读出来是调色板索引,不是 RGB,这点要注意,别拿 RGB 值去比对类别。
2.3 类别体系设计:二分类还是多分类,决定了后面所有工作
宠物数据集常见的类别体系有三种。第一种是二分类:背景 0、宠物 1,最简单,适合抠图、宠物检测前置。第二种是物种级多分类:背景、猫、狗、其他宠物,适合宠物摄像头区分物种。第三种是部位级:背景、头部、躯干、四肢、尾巴,适合姿态相关分析。7000 张的体量,我建议二分类或物种级,部位级标注成本高、样本摊薄后每类可能只剩一两千张,训不充分。
类别体系一旦定了,后续的 loss 函数、评价指标、可视化配色都跟着定。多分类要用交叉熵或带 softmax 的 dice,二分类用 BCE 或 sigmoid dice。评价指标上,多分类看 mIoU,二分类看 IoU 和 Dice。这些不是可选项,是必须对齐的。我见过有人拿二分类标签去跑多分类的 softmax,训练 loss 一直不降,查了半天才发现类别数设错了,这种坑纯属没做前面的检查。
3. 用 7000 张宠物图跑通第一个分割模型:从划分到训练
3.1 训练集验证集划分:别让同一只宠物同时出现在两边
数据划分看着简单,宠物数据集有个隐蔽的坑:同一只宠物可能被拍了多张,如果随机划分,同一只猫的照片可能既在训练集又在验证集,验证指标会虚高,模型其实没学到泛化。常见做法是按宠物个体或按拍摄场景分组划分,同一组只进一个集合。如果数据集没提供个体 ID,退而求其次按图像相似度聚类后再划分,或者至少保证验证集占 15% 到 20%。
import os import random import shutil random.seed(42) img_dir = "pet_seg/images" mask_dir = "pet_seg/masks" out_root = "pet_seg_split" files = sorted(os.listdir(img_dir)) random.shuffle(files) split = int(len(files) * 0.85) train_files = files[:split] val_files = files[split:] for subset, flist in [("train", train_files), ("val", val_files)]: os.makedirs(f"{out_root}/{subset}/images", exist_ok=True) os.makedirs(f"{out_root}/{subset}/masks", exist_ok=True) for f in flist: stem = os.path.splitext(f)[0] shutil.copy(os.path.join(img_dir, f), f"{out_root}/{subset}/images/{f}") shutil.copy(os.path.join(mask_dir, stem + ".png"), f"{out_root}/{subset}/masks/{stem}.png") print("train:", len(train_files), "val:", len(val_files))固定随机种子 42 是为了可复现,团队协作时大家划分一致。85/15 的切分在 7000 张规模下,验证集约 1050 张,足够稳定评估。如果你的数据里有明显的个体聚集,把random.shuffle换成按组打乱,逻辑一样,只是打乱单位从单张图变成组。
3.2 数据增强:宠物毛发场景下哪些增强有效、哪些是负优化
语义分割的数据增强必须图像和标签同步变换,这是铁律。宠物图像里,水平翻转、随机裁剪、亮度对比度扰动是安全且有效的。垂直翻转要谨慎,猫狗倒过来的样本在真实场景里极少,加了可能引入噪声。旋转超过 30 度同理。颜色抖动幅度别太大,毛发颜色是重要特征,抖过头模型学不到稳定表征。
import albumentations as A import cv2 train_transform = A.Compose([ A.HorizontalFlip(p=0.5), A.RandomResizedCrop(size=(512, 512), scale=(0.7, 1.0), p=1.0), A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=0.5), A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)), ]) val_transform = A.Compose([ A.Resize(height=512, width=512), A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)), ]) # 使用示例 img = cv2.imread("pet_seg/images/0001.jpg") img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask = cv2.imread("pet_seg/masks/0001.png", cv2.IMREAD_GRAYSCALE) aug = train_transform(image=img, mask=mask)albumentations 的 Compose 会自动把几何变换同步应用到 image 和 mask 上,这是它比手写增强靠谱的地方。RandomResizedCrop 的 scale 下限设 0.7,保证裁出来的区域还有足够宠物像素,裁太狠可能整张图只剩背景,标签全 0,这种样本对训练是干扰。Normalize 用的 ImageNet 均值方差,如果你用 ImageNet 预训练权重就必须保持一致,从头训可以换成数据集自身统计值。
3.3 选哪个分割网络:U-Net、DeepLabV3+ 还是 SegFormer
7000 张的规模,我一般推荐两条路线。想快速出结果、显存有限,用 U-Net 加 ResNet34 或 EfficientNet-B0 编码器,结构简单,512 分辨率下单卡 8G 就能跑。追求精度、有预训练权重加持,用 SegFormer 的 B0 或 B1 版本,Transformer 架构对毛发这种长程依赖边界处理得更好,但显存和训练时间都要多一档。DeepLabV3+ 介于两者之间,空洞卷积对多尺度目标友好,宠物大小差异大时表现稳。
选型不是越新越好。SegFormer 在小数据集上如果没加载预训练,很容易过拟合;U-Net 从头训反而更稳。我的习惯是先用 U-Net 跑一个 baseline,记录 mIoU 和 Dice,再换 SegFormer 对比,看提升值不值得多花的算力。7000 张这个量级,两者差距通常在 2 到 5 个 mIoU 点,如果业务对边界精度要求不高,U-Net 就够了。
3.4 训练配置与关键参数:学习率、损失函数、batch size 怎么定
训练配置里最容易翻车的是学习率和损失函数。用预训练编码器时,编码器学习率要设小,通常是解码器的十分之一,否则预训练权重被冲垮。损失函数上,宠物分割边界像素占比小,纯交叉熵会被背景主导,常见做法是交叉熵加 Dice 的加权和,Dice 负责拉边界,交叉熵负责整体稳定。
import torch import torch.nn as nn class CombinedLoss(nn.Module): def __init__(self, ce_weight=0.5, dice_weight=0.5): super().__init__() self.ce = nn.CrossEntropyLoss() self.ce_weight = ce_weight self.dice_weight = dice_weight def dice_loss(self, pred, target, eps=1e-6): # pred: [B, C, H, W] logits, target: [B, H, W] prob = torch.softmax(pred, dim=1) target_onehot = torch.nn.functional.one_hot( target, num_classes=pred.shape[1]).permute(0, 3, 1, 2).float() inter = (prob * target_onehot).sum(dim=(0, 2, 3)) union = prob.sum(dim=(0, 2, 3)) + target_onehot.sum(dim=(0, 2, 3)) dice = (2 * inter + eps) / (union + eps) return 1 - dice.mean() def forward(self, pred, target): return self.ce_weight * self.ce(pred, target) + \ self.dice_weight * self.dice_loss(pred, target) # 优化器:编码器和解码器分组学习率 encoder_params = list(model.encoder.parameters()) decoder_params = list(model.decoder.parameters()) optimizer = torch.optim.AdamW([ {"params": encoder_params, "lr": 1e-5}, {"params": decoder_params, "lr": 1e-4}, ], weight_decay=1e-4)CombinedLoss 里 ce_weight 和 dice_weight 各 0.5 是起点,如果发现边界糊,把 dice 权重提到 0.6 到 0.7;如果整体类别预测乱,提交叉熵权重。dice_loss 里对每个类别单独算再平均,这样小类别不会被大类别淹没。优化器用 AdamW,编码器 1e-5、解码器 1e-4 是预训练微调的常规配置,从头训可以统一用 1e-3 配余弦退火。batch size 在 512 分辨率下,8G 显存大概能放 4 到 8,用梯度累积凑等效大 batch 也行。
4. 训练不收敛、边界糊、指标虚高:宠物分割的排查清单
4.1 现象:loss 震荡不下降,验证集 mIoU 卡在 0.3 以下
原因通常有三个。一是标签像素值和模型输出类别数不匹配,比如标签里有 0、1、2 但模型输出只设了 2 类,第 2 类永远学不到。二是学习率太大,预训练权重被冲垮,表现为前几个 epoch loss 先降后猛涨。三是数据增强把标签也做了插值,mask 出现非整数像素值,交叉熵直接报错或算出垃圾。
解决:先跑 2.2 的检查脚本确认类别体系,再打印一个 batch 的标签唯一值,和模型输出通道数对齐。学习率从 1e-5 起试,观察前 5 个 epoch 的 loss 曲线。mask 的增强只允许最近邻插值,albumentations 里 mask 默认就是最近邻,但如果你手动 resize 用了双线性,就会出问题。
4.2 现象:整体 mIoU 还行,但宠物边缘像狗啃的
这是宠物分割最典型的翻车现场。原因一是损失函数里 Dice 权重太低,边界像素被交叉熵忽略。原因二是下采样倍率太高,编码器把细节丢光了,解码器补不回来。原因三是标签本身边界就粗糙,标注时毛发区域被草草涂了个大概。
解决:把 dice_weight 提到 0.6 以上,或者加边界感知损失。网络侧用输出 stride 为 4 而不是 8 的解码器,保留更多空间细节。如果标签质量本身差,先抽样看几十张标签,边界糊得离谱的样本考虑剔除或重标,别指望模型学会连人都标不准的边界。
4.3 现象:验证集指标高得离谱,上线后效果崩了
几乎可以断定是数据泄漏。同一只宠物的多张照片被分到了训练和验证两边,模型记住了这只宠物的特征,验证集等于开卷考试。另一种可能是验证集太小或分布和训练集太像,比如全是同一背景、同一光照。
解决:回到 3.1,按个体或场景分组划分。检查方法很简单,算一下训练集和验证集的图像相似度,如果大量验证图和训练图高度相似,就是泄漏。验证集要尽量覆盖不同品种、不同光照、不同背景,7000 张里至少留出几百张有明显差异的。
4.4 现象:训练时显存爆了,或者 batch size 只能设 1
原因通常是分辨率设太高,或者模型解码器用了全分辨率特征拼接。512 分辨率下 U-Net 加 ResNet34 大概占 4 到 6G,如果你设了 1024 又用 SegFormer,24G 也扛不住。
解决:优先降分辨率到 512 或 384,宠物分割对分辨率没那么敏感,384 通常够用。其次用混合精度训练,显存能省三到四成。再不行就梯度累积,batch size 设 2,累积 8 步等效 16。别硬扛大分辨率,先跑通再优化。
4.5 现象:某些类别 IoU 一直是 0
小类别样本太少,或者类别像素值在标签里根本没出现。7000 张里如果某个宠物品种只有几十张,模型学不到。也可能是标签里该类用了别的像素值,和你 classes.txt 对不上。
解决:统计每个类别的像素占比和图像数,占比低于 1% 的类别考虑合并或过采样。确认标签像素值和类别定义一致,用 2.2 的脚本打印唯一值集合,逐个核对。类别不平衡严重时,交叉熵里加类别权重,权重和频率成反比。
5. 把 7000 张用到极致:预训练、软标签与边界后处理
数据集规模有限时,榨干它的方式不是反复调网络结构,而是换训练策略。我常用的第一招是自监督或大规模预训练权重打底,ImageNet 预训练是标配,如果能拿到 COCO 或 ADE20K 上训过的分割权重,在宠物数据上微调,mIoU 通常能比从头训高 5 到 10 个点。第二招是软标签,宠物毛发边界本身是渐变的,硬标签把边界像素非 0 即 1,模型学得很痛苦。把边界区域用高斯模糊生成软标签,让模型输出概率而不是硬分类,边界过渡会自然很多,这也是热词里「软标签」在分割任务里的实际用法。
import numpy as np import cv2 def soft_boundary(mask, kernel_size=5, sigma=1.5): # mask: 硬标签 [H, W],返回软标签 [H, W] float fg = (mask > 0).astype(np.float32) soft = cv2.GaussianBlur(fg, (kernel_size, kernel_size), sigma) # 内部区域保持硬标签,只软化边界 eroded = cv2.erode(fg, np.ones((kernel_size, kernel_size), np.uint8)) soft = np.where(eroded > 0, 1.0, soft) return soft这段逻辑是:先二值化前景,高斯模糊得到边界渐变,再用腐蚀把目标内部还原成硬 1,只保留边界带的软过渡。kernel_size 和 sigma 控制软化宽度,5 和 1.5 适合 512 分辨率,边界带大概两三个像素。软标签训练时损失函数换成 BCE 或带 soft target 的交叉熵,模型输出用 sigmoid 而不是 softmax。
第三招是边界后处理。推理完得到概率图,用条件随机场或简单的形态学操作修边界,能再捞回一两个 mIoU 点。CRF 对毛发这种细碎边界效果明显,但速度慢,实时场景慎用。轻量替代是引导滤波,用原图当引导,把概率图边缘对齐到图像真实边缘上。
验证这套流程是否有效,别只看 mIoU。我习惯额外看三个东西:边界带的 IoU(把标签腐蚀掉内部只留边界算指标)、可视化叠加图的目视效果、以及在不同品种宠物上的分组指标。整体 mIoU 涨了但某个品种掉了,说明模型在偏科,得回去查数据分布。
最后说个我自己的习惯:每次拿到新数据集,先花半天做数据体检,别急着开训。7000 张宠物图,体检能查出标签错误、类别体系混乱、数据泄漏这些致命问题,省下的调试时间远超这半天。训崩了再回头查数据,成本高得多。希望帮到你。
本文还有配套的精品资源,点击获取