news 2026/9/28 1:27:07

约7000张宠物语义分割数据集:三类别标签映射与UNet训练避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
约7000张宠物语义分割数据集:三类别标签映射与UNet训练避坑指南

简介:本资源为面向图像分割学习者的宠物语义分割数据集,约7000张图像及对应标签,适合入门与进阶的深度学习实践者用于训练、验证分割模型。数据集已按训练集与验证集划分:训练集约5100张图片及mask,验证集约2200张图片及mask,标签涵盖背景、宠物、边缘三类,像素值定义可参考classes文件。压缩包共2000个文件,以1998个png图像与掩膜为主,另含1个txt类别说明和1个py可视化脚本,整体约765.67MB,采用7z格式打包。配套脚本可随机抽取一张图片,展示原图、GT图像及GT在原图上的蒙板效果并保存至当前目录,便于快速检查标注质量与数据分布。目前已有48人学习,适合需要现成数据开展UNet、SwinUnet、TransUnet等分割网络实验的读者。

1. 约 7000 张宠物语义分割数据集:拿到手先别急着喂给 UNet

如果你正在复现 DeepLabV3+、UNet 或者 Swin-Unet,却卡在「找不到一份干净、已经划分好、mask 还是像素级标签」的数据集上,这份约 7000 张的宠物图像语义分割数据集值得先看一眼。它把训练集和验证集都拆好了,训练集约 5100 张、验证集约 2200 张,每张原图都配一张同名 mask,类别只有三类:背景、宠物主体、边缘。文件名像Egyptian_Mau_182.png、Bombay_140.png、Abyssinian_32.png这种,一眼能看出是猫的品种命名,说明它大概率是从 Oxford-IIIT Pet 那套数据整理出来的语义分割版本。

它解决的核心问题不是「数据量不够」,而是「省掉你自己写划分脚本、对齐文件名、处理 mask 像素值映射」这几步脏活。适合两类人:一类是刚入门语义分割、想跑通一个三类别 baseline 的新手;另一类是手里有医学图像分割网络(UNet、TransUNet、Swin-Unet 改进版)想换个数据集验证泛化性的熟手。下面按「这份资源是什么 → 怎么读标签 → 怎么接进训练 → 坑在哪 → 怎么验证」的顺序拆开讲,每一步都能直接抄。

2. 读懂 mask 像素值与 classes 映射:三类标签到底怎么存的

2.1 为什么先看 classes 文件而不是先写 DataLoader

很多人拿到分割数据集第一反应是torchvision.datasets套一下就开始训,结果 loss 一直不降,最后发现 mask 里存的是1/2/3而不是0/1/2,或者边缘类被当成了独立前景。这份数据集的类别定义是「背景、宠物、边缘」三类,但像素值标签的具体数值必须参考随包的 classes 文件,不能凭经验假设。语义分割里 mask 的像素值就是类别索引,差一个偏移量,交叉熵就会把背景学成宠物。

常见做法是先用一段脚本把 mask 的唯一值统计出来,和 classes 文件对照,确认映射关系再动手。这一步花两分钟,能省掉后面几小时的玄学调参。

import numpy as np from PIL import Image import os mask_dir = "train/masks" # 换成你的 mask 目录 classes_file = "classes.txt" # 随包的类别定义文件 # 1. 读 classes 文件,看官方给的类别顺序 with open(classes_file, "r", encoding="utf-8") as f: class_names = [line.strip() for line in f if line.strip()] print("类别顺序:", class_names) # 2. 统计所有 mask 的像素唯一值,确认实际存储的标签值 uniq = set() for name in os.listdir(mask_dir)[:200]: # 抽样 200 张足够 m = np.array(Image.open(os.path.join(mask_dir, name))) uniq.update(np.unique(m).tolist()) print("mask 实际像素值:", sorted(uniq))

逻辑说明:第一段读 classes 文件拿到官方类别顺序,第二段抽样统计 mask 的真实像素值。参数上[:200]是抽样数量,7000 张全扫也行,只是慢;np.unique返回的是该 mask 里出现过的所有类别值。如果打印出来是[1, 2, 3]而 classes 是[background, pet, edge],那训练时要么在 Dataset 里减 1,要么把ignore_index和权重重新配。

2.2 边缘类要不要单独算 loss

三类里「边缘」这一类是最容易被忽略的。它本质上是宠物轮廓附近的过渡像素,占比很小,如果直接和背景、宠物一起做普通交叉熵,边缘类几乎学不到,因为梯度被大面积的背景和宠物主体淹没了。常见做法有两种:一是给边缘类更高的 class weight,二是把边缘当作辅助任务,主 loss 只算背景和宠物,边缘单独算一个二值 loss。

我一般会先跑一版不加权重的 baseline,看验证集上边缘类的 IoU。如果边缘 IoU 长期低于 0.1,就说明它被淹没了,这时候再上权重。权重不要拍脑袋设,用1 / sqrt(freq)这种按频率倒数开方的经验公式先试:

import numpy as np # 假设统计出的三类像素占比(按你的实际数据替换) freq = np.array([0.85, 0.13, 0.02]) # 背景、宠物、边缘 weight = 1.0 / np.sqrt(freq) weight = weight / weight.sum() * 3 # 归一化到均值为 1 附近 print("建议 class weight:", weight)

参数说明:freq是三类在全体 mask 里的像素占比,边缘类通常只有百分之几甚至更低;1/sqrt比直接1/freq温和,避免边缘类权重过大导致训练震荡。算出来的 weight 直接传给nn.CrossEntropyLoss(weight=...)。注意这只是起点,最终还要看验证集指标微调。

2.3 文件名对齐是隐形的坑

数据集里原图和 mask 是同名不同目录,比如images/Egyptian_Mau_182.png对应masks/Egyptian_Mau_182.png。写 Dataset 时最稳的做法是遍历 images 目录,用os.path.splitext换目录去拼 mask 路径,而不是分别遍历两个目录再排序配对——后者一旦有一张图缺 mask,整个索引就错位了,而且不会报错,只会让模型学出莫名其妙的输出。

import os from PIL import Image from torch.utils.data import Dataset class PetSegDataset(Dataset): def __init__(self, root, split="train", transform=None): self.img_dir = os.path.join(root, split, "images") self.mask_dir = os.path.join(root, split, "masks") self.names = sorted(os.listdir(self.img_dir)) self.transform = transform def __len__(self): return len(self.names) def __getitem__(self, idx): name = self.names[idx] img = Image.open(os.path.join(self.img_dir, name)).convert("RGB") mask_name = os.path.splitext(name)[0] + ".png" # 强制同名 mask = Image.open(os.path.join(self.mask_dir, mask_name)) if self.transform: img, mask = self.transform(img, mask) return img, mask

逻辑说明:以 images 目录的文件名为准,用splitext拼出 mask 名,保证一一对应。参数上split控制读 train 还是 val,transform需要同时处理 img 和 mask(几何变换要同步,颜色变换只对 img)。如果某张 mask 缺失,这里会直接抛FileNotFoundError,比静默错位好排查得多。

3. 接进 UNet / Swin-Unet 训练:dataloader、归一化与可视化脚本

3.1 训练集与验证集的目录结构确认

这份数据集已经划分好,训练集约 5100 张、验证集约 2200 张,结构是每个 split 下各有images和masks两个目录。动手前先tree一下确认层级,别把train/images和val/images搞混。常见做法是写一个常量字典管理路径,避免到处硬编码字符串。

# 确认目录结构,只看两层 find . -maxdepth 3 -type d | sort # 期望输出类似: # ./train # ./train/images # ./train/masks # ./val # ./val/images # ./val/masks

如果发现 mask 和 image 不在同一层级,或者多套了一层,先手动整理成上面这种结构再往下走。分割任务对路径结构很敏感,路径错了往往不报错,只是读到空数据。

3.2 归一化参数别照搬 ImageNet

宠物图像是自然图像,用 ImageNet 的 mean/std 做归一化通常没问题,但如果你要做迁移学习、加载的是在 ImageNet 上预训练的 backbone,那就必须用 ImageNet 的统计量,否则预训练权重等于白搭。反过来,如果你从零训一个小 UNet,用数据集自己的 mean/std 会更稳。常见做法是先算一遍训练集的均值和标准差:

import numpy as np from PIL import Image import os img_dir = "train/images" pixel_sum = np.zeros(3) pixel_sq_sum = np.zeros(3) n = 0 for name in os.listdir(img_dir): arr = np.array(Image.open(os.path.join(img_dir, name)).convert("RGB")) / 255.0 pixel_sum += arr.reshape(-1, 3).sum(axis=0) pixel_sq_sum += (arr.reshape(-1, 3) ** 2).sum(axis=0) n += arr.shape[0] * arr.shape[1] mean = pixel_sum / n std = np.sqrt(pixel_sq_sum / n - mean ** 2) print("mean:", mean, "std:", std)

参数说明:/255.0把像素压到 0~1 再统计;n是总像素数。算出来的 mean/std 直接填进transforms.Normalize。注意验证集要用训练集的统计量,不能各算各的,否则分布不一致。

3.3 可视化脚本怎么用、输出在哪

数据集自带一个可视化脚本,随机抽一张图,把原始图、GT 图、GT 在原图上的蒙板三张展示出来,并保存在当前目录。这个脚本的价值在于快速验证「mask 和原图是否对齐」——如果蒙板盖错了位置,说明文件名配对或 resize 出了问题。跑之前确认脚本里的路径指向你的数据根目录,跑完在当前目录找输出的 png。

# 常见调用方式,具体参数看脚本内的 argparse python visualize.py --root ./ --split val --num 3

如果脚本没有 argparse、路径写死,就手动改root和split两个变量。输出一般是vis_xxx.png这种三合一对比图。重点看第三张蒙板图:宠物轮廓应该被准确覆盖,边缘类如果被可视化出来,应该是一圈细线。如果蒙板整体偏移或缩放不对,八成是原图和 mask 尺寸不一致,或者 transform 里对 mask 用了插值。

提示:可视化脚本跑通再开始训练。很多人跳过这步,训了半天发现 mask 根本没对上,血泪经验。

3.4 训练循环里必须盯的三个指标

接进 UNet 或 Swin-Unet 后,训练循环里别只看 loss。语义分割至少盯三个:整体像素准确率(容易被背景刷高,参考价值有限)、mIoU(真正反映分割质量)、以及每一类的 IoU(尤其边缘类)。常见做法是每个 epoch 在验证集上算一次混淆矩阵,再从中导出各类 IoU。

import torch import numpy as np def compute_iou(pred, target, num_classes=3): # pred, target: [B, H, W] 的整数标签 ious = [] pred = pred.flatten() target = target.flatten() for c in range(num_classes): inter = ((pred == c) & (target == c)).sum().item() union = ((pred == c) | (target == c)).sum().item() ious.append(inter / union if union > 0 else float("nan")) return ious # 训练循环里 model.eval() all_pred, all_gt = [], [] with torch.no_grad(): for img, mask in val_loader: img = img.cuda() out = model(img) pred = out.argmax(dim=1).cpu() all_pred.append(pred) all_gt.append(mask) pred = torch.cat(all_pred) gt = torch.cat(all_gt) ious = compute_iou(pred, gt, num_classes=3) print("各类 IoU:", ious, "mIoU:", np.nanmean(ious))

逻辑说明:argmax(dim=1)把网络输出的 logits 转成类别索引;混淆矩阵按类累加 inter 和 union。参数num_classes=3对应背景、宠物、边缘。注意union==0时返回 nan,最后用nanmean忽略。如果边缘类 IoU 一直是 nan,说明验证集里边缘像素太少或根本没预测出来,要回头查标签映射。

4. 避坑与排查:这份数据集最容易翻车的五个地方

4.1 现象:loss 正常下降但 mIoU 卡在 0.3 上不去

原因:mask 像素值和类别索引差了一个偏移量,模型学的是「背景=1、宠物=2」,但评估时按「背景=0、宠物=1」算,导致预测和 GT 系统性错位。解决:回到 2.1 的统计脚本,确认 mask 唯一值,在 Dataset 里统一做mask = mask - 1或按 classes 文件重映射,然后重新评估。

4.2 现象:边缘类 IoU 长期为 0

原因:边缘像素占比极低,普通交叉熵下梯度被淹没;或者可视化时发现边缘根本没被标注出来。解决:先确认 mask 里确实存在边缘类像素值(用 2.1 的脚本看唯一值是否包含边缘那一类),再上 class weight 或把边缘拆成辅助二值任务。如果数据本身边缘标注就稀疏,别强求,把它当正则项用。

4.3 现象:训练时显存爆掉,batch size 只能设 2

原因:原图分辨率偏高,UNet 下采样再上采样,中间特征图很吃显存。解决:常见做法是训练时随机裁剪到 256×256 或 320×320,验证时再整图推理;或者用混合精度torch.cuda.amp。注意裁剪要 img 和 mask 同步,别只裁 img。

4.4 现象:验证集指标比训练集高很多

原因:验证集只有 2200 张,且可能和训练集同分布,加上验证时用了model.eval()关掉 dropout,指标虚高很正常。解决:别被验证集骗了,留一小部分训练数据做 holdout,或者做 K 折。另外检查验证集有没有和训练集重复的图,同名不同 split 的情况要排掉。

4.5 现象:可视化脚本输出的蒙板整体偏移

原因:原图和 mask 尺寸不一致,transform 里对 mask 用了双线性插值,导致标签值被插成小数。解决:mask 的 resize 必须用最近邻InterpolationMode.NEAREST,几何变换(旋转、裁剪)要和 img 用同一组随机参数。检查transforms里 mask 那条链路,别让它走默认插值。

5. 进阶验证:用混淆矩阵和单类 IoU 判断数据集值不值得长期用

跑通 baseline 之后,真正决定要不要把这份数据集长期用下去的,不是 mIoU 那一个数,而是每一类的表现和混淆矩阵的结构。我一般会做两件事:一是把验证集的混淆矩阵画出来(用表格也行),看背景和宠物之间有没有大量互混;二是单独看边缘类的召回,判断它到底是「学不会」还是「标注本身就不一致」。

先算混淆矩阵。下面这段直接输出 3×3 的计数表,行是 GT、列是预测:

import torch import numpy as np def confusion_matrix(pred, target, num_classes=3): # pred, target: 一维整数张量 idx = target * num_classes + pred cm = torch.bincount(idx, minlength=num_classes ** 2) return cm.reshape(num_classes, num_classes).numpy() # 假设 pred, gt 已经 concat 好并 flatten cm = confusion_matrix(pred.flatten(), gt.flatten(), num_classes=3) print("混淆矩阵(行=GT, 列=Pred):\n", cm) # 从混淆矩阵导出每类 IoU for c in range(3): tp = cm[c, c] fp = cm[:, c].sum() - tp fn = cm[c, :].sum() - tp iou = tp / (tp + fp + fn) if (tp + fp + fn) > 0 else float("nan") print(f"类 {c} IoU: {iou:.4f}")

逻辑说明:target * num_classes + pred把二维的 (GT, Pred) 组合压成一维索引,bincount一次统计完所有组合,再 reshape 回矩阵。参数num_classes=3对应三类。看矩阵时重点看对角线:对角线越大越好;如果cm[0,1](背景被预测成宠物)很大,说明模型把大片背景误判成宠物,通常是类别不平衡或归一化有问题。

拿到每类 IoU 后,判断标准可以这样定:背景 IoU 通常最高,宠物主体 IoU 在 0.7 以上算这份数据可用,边缘类如果低于 0.2 且召回也低,就把它降级成辅助任务,别让它拖累主任务。如果宠物主体 IoU 都上不去 0.6,先别怀疑网络,回头查标签映射和可视化对齐——这份数据集本身质量不差,问题多半出在读取环节。

还有一个容易被忽略的验证点:把训练集和验证集的类别像素占比分别统计一遍,对比两者分布。如果验证集的边缘占比和训练集差一个数量级,说明划分可能不是随机的,这时候要么重新划分,要么在评估时对边缘类单独加权。我一般会写个小函数把两个 split 的三类占比打出来,一眼就能看出分布是否一致。

def class_ratio(mask_dir, num_classes=3, sample=300): import os from PIL import Image counts = np.zeros(num_classes) names = os.listdir(mask_dir)[:sample] for name in names: m = np.array(Image.open(os.path.join(mask_dir, name))) for c in range(num_classes): counts[c] += (m == c).sum() return counts / counts.sum() print("train 占比:", class_ratio("train/masks")) print("val 占比:", class_ratio("val/masks"))

参数sample=300是抽样数量,想更准就全量。两个占比向量越接近,说明划分越均衡。如果 val 的边缘占比明显偏高或偏低,评估指标的可比性就要打折扣。

从那以后我每次拿到新的分割数据集,都强制先跑一遍「唯一值统计 + 可视化对齐 + 双 split 占比对比」这三步,再动网络。这份约 7000 张的宠物语义分割数据集,结构清晰、划分现成、还带可视化脚本,作为三类别分割的练手或验证集是够格的,前提是你把标签映射和 mask 对齐这两件事做扎实。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 1:26:26

MT25QL256 SPI NOR FLASH驱动实战:从ID读取到4KB扇区擦除

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 1:26:17

超声乳腺图像分割实战:从数据集预处理到U-Net训练全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 1:26:16

Vim实战:用编辑器高效处理图像分类任务全流程

简介:本资源面向计算机视觉方向的开发者与研究者,提供使用Vim视觉模型完成图像分类任务的完整工程包。Vim凭借计算与内存效率高、处理高分辨率图像能力强的特点,被视为下一代视觉基础模型的理想选择,适合希望复现轻量级视觉骨干网…

作者头像 李华
网站建设 2026/9/28 1:26:16

SC7A20三轴加速度计I2C驱动开发实战:从时序到滤波

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 1:26:13

计算机408中断系统详解:核心部件、响应流程与多重中断屏蔽字

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 1:24:55

YOLOv5知识蒸馏实战:从教师模型到损失函数设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华