☰
猫狗图像分割数据集构建与训练测试集划分实战
2026/9/28 1:03:19 网站建设 项目流程

简介:深度学习图像分割入门与训练可用的猫狗图像分割数据集,面向需要现成样本划分的算法学习者与研究者。数据集中图像与掩码一一对应,包含训练集5912张、测试集1478张,共7390对样本,覆盖多个猫狗品种,前景区域丰富且标注细致,可直接训练分割网络,免去自行标注与拆分的预处理工作;目录结构清晰,训练集和测试集下均设图像目录与掩码目录,便于加载与批处理。包内另附可视化Python脚本,可随机抽取一张图,将原始图片、GT图像及GT蒙板叠加效果展示并保存,便于快速检查标注质量或用于论文展示。资源以zip压缩包形式提供,共2000个文件,以jpeg图片为主,另有1个py脚本,压缩包大小约230.81MB。目前已有236人浏览学习,适合用于课程作业、算法对比实验或分割模型快速验证。

1. 猫狗图像分割数据集:训练集和测试集的划分是整个项目的第一道分水岭

很多刚开始接触深度学习图像分割的人,把注意力全放在模型结构上,U-Net还是DeepLabV3+,学习率调到多少,backbone换成谁。但真实流程踩下来会发现,卡住进度的往往不是模型,而是数据集本身。猫狗图像分割数据集不是一堆猫狗照片加一个txt标签,它要求原图、逐像素掩码、训练集和测试集三者严格对齐。划分训练集和测试集时,如果没处理好同场景图片的泄漏,后面调参数全是白调。这篇笔记围绕“自建一套已划分训练集和测试集的猫狗图像分割数据集”完整走一遍:标注格式怎么选、图片怎么清洗、划分脚本怎么写、以及常见坑位在哪,适合准备拿深度学习做图像分割入门、或想造私有数据集的开发者照着走。

2. 猫狗图像分割数据集的结构与标注格式:动手前先确认四件事

2.1 一张猫图配一张掩码:图像分割数据集的最小单元

图像分割和图像分类、目标检测的本质区别,在于输出粒度。分类给整张图一个标签,检测给目标一个框,分割要给每个像素一个类别。对应到数据上,一套分割数据集的最小单元是“一对文件”——原图和掩码。原图就是普通RGB照片,掩码是尺寸完全相同的单通道PNG图,每个像素存一个类别ID。猫狗分割里常用0表示背景、1表示猫、2表示狗;如果只做“把猫狗从背景里抠出来”的二分类,掩码只用0和1就够。

第一次自建数据集的开发者,几乎都会在掩码格式上踩坑:把掩码存成JPEG。JPEG是有损压缩,猫毛边缘会被压出一圈连续的过渡灰色,像素值既不是0也不是1。训练时模型把这层灰色当独立类别去学,最终分割边缘会糊成一片。掩码必须用PNG这类无损格式,保证像素值精确无误。这是整个数据集环节里最不该省的成本。

提示:加载掩码后第一件事是打印np.unique(mask),确认实际取值。标注工具导出的掩码经常把前景存成255而不是1,如果不做归一化,模型的输出类别数会比你预期的多一个。

2.2 三种标注格式怎么选:PNG掩码、Labelme JSON和COCO

自建猫狗分割数据集,标注阶段的中间产物和训练阶段的数据文件往往是两种格式。标注时用Labelme这类工具,产出的是JSON格式的多边形顶点;训练前要把JSON转成PNG掩码;如果后续要接MMSegmentation或其他开源工具链,则可能要转COCO格式。

三种格式的取舍,我这里给一张对比表:

| 格式 | 内容 | 优点 | 缺点 | 适用阶段 | | PNG掩码 | 每个像素存类别值 | 训练加载无解析开销 | 不可逆,改一笔要返工 | 训练文件 | | Labelme JSON | 多边形顶点坐标 | 可再编辑,标注友好 | 训练前必须转掩码 | 标注中间产物 | | COCO JSON | 多边形或RLE编码 | 框架生态完善,工具链多 | 结构重,手写容易出错 | 对接现有工具链 |

一个人标几百张图的话,我的建议是Labelme标注、PNG掩码训练,暂时不碰COCO。COCO转换脚本网上很多,哪天有了明确对接需求再转也不迟。反过来,如果团队用的是能直接输出COCO的标注平台,那就顺着平台走,写个COCO转PNG的脚本喂给训练,千万不要做二次标注。

2.3 划分训练集和测试集:同一只猫不能同时出现在两边

划分训练集和测试集时,很多人都是按文件名做一次随机打乱,八二开就开跑。这在分类数据集上勉强能忍,在图像分割数据集上是埋雷。

典型翻车场景:你从一段视频里抽帧,同一只狗在画面里回头三次,三帧高度相似。随机打乱后,两帧进了训练集,一帧进了测试集——训练时模型等于提前见过测试题的答案,测试集指标虚高,部署到真实新场景立刻现原形。

正确做法是先把图片按“来源”分组,再对组做划分。同一次拍摄、同一只猫狗的所有图,要么全进训练集,要么全进测试集。实现上我习惯在文件名里加组前缀,比如group01_cat_001.jpg,划分脚本只对组ID做shuffle,组内不拆散。

比例上也没有放之四海皆准的值。分类任务常说的8:2指训练集和测试集,但图像分割我建议再单独划一个验证集,因为分割模型的超参数多,得靠验证集调参、做早停。常用比例是7:2:1或6:2:2。数据总量低于两三百张时,验证集别切太小,否则loss曲线抖得看不出收敛方向。

2.4 拿到数据集先做统计:三步看清这套数据值不值得往下做

划分完第一件事不是开训,是看一眼类别的分布。我一般会跑一段最短的统计脚本:

import os import numpy as np from PIL import Image mask_dir = "dataset/masks/train" sample_count = {} for f in os.listdir(mask_dir): if not f.endswith(".png"): continue mask = np.array(Image.open(os.path.join(mask_dir, f))) for v in np.unique(mask): sample_count[int(v)] = sample_count.get(int(v), 0) + 1 print(sample_count) # {0: 58, 1: 42, 2: 36}

这段代码统计的是“每个类别出现在多少张图里”,不是像素总数。想看像素级占比,用np.bincount(mask.ravel())另外算。前一个数反映样本层面的类别均衡性,后一个数暴露分割任务里的老问题:背景像素动辄占九成以上,这个要在训练时用损失函数权重或采样来平衡。

如果某一类只出现在寥寥几张图里,直接训练等于让模型用玄学猜。优先补数据,补不了就把这类并进背景再训练。硬着头皮训出来的模型,测试集表现往往也是靠运气撑着的,换个场景就原形毕露。

3. 从零构建猫狗分割数据集:采集、清洗、标注一套打通

3.1 图片采集与初筛:哪些图放进训练集就是给自己埋雷

做私有数据集的素材来源,常见有三种:自己拍摄、网络公开图、拿现有公开分类数据集二次加工。自己拍摄数据最干净但量少;网络爬图要盯紧授权,商用项目尽量别碰;开源猫狗分类数据集拿来练手最快,但那些数据集大多只有分类标签没有掩码,一样要过一遍标注关。

初筛要做的基础检查是三件事。第一是分辨率,分割模型输入常见256到512,最小边低于256的图直接放弃,否则resize后猫脸细节全没了。第二是遮挡情况,目标被大面积遮挡、两只猫互相叠在一起的图不要,标注时边界都判不齐,训练时模型学的是不确定性。第三是水印和滤镜过重,水印会让模型学到“水印等于猫”的杂念,滤镜改变颜色分布,给后续数据增强添乱。

批量过一遍最小边过滤,一段Python就够:

import os from PIL import Image src_dir = "raw/" keep_dir = "clean/" bad = [] for root, _, files in os.walk(src_dir): for name in files: if not name.lower().endswith((".jpg", ".jpeg", ".png")): continue path = os.path.join(root, name) img = Image.open(path) w, h = img.size if min(w, h) < 256: bad.append((path, f"{w}x{h}")) else: os.makedirs(keep_dir, exist_ok=True) img.save(os.path.join(keep_dir, name)) print("removed:", len(bad))

这段代码只做了尺寸过滤。想再滤掉模糊图,可以给每张图算一下Laplacian方差,数值低说明图像柔和模糊,直接剔除。模糊图的边缘信息是脏的,分割模型学到的边界会飘。我第一次做数据时偷懒跳过这步,结果训练loss怎么都降不下去,回头洗数据花了一整天,这是真金白银换来的教训。

3.2 标注猫狗轮廓:Labelme里那些决定成败的细节

清洗完进入标注。常用流程是pip install labelme,写一个labels.txt,里面依次放背景、猫、狗三行,然后启动labelme images/ --labels labels.txt开始标。

标注规范直接影响模型质量,值得注意的有三点。

第一,多实例必须设group_id。一张图里有两只猫,如果都给cat标签但不设group_id,Labelme会把它们当成一个对象,生成的掩码就会把两只猫黏成一大块,模型永远学不会分开两只紧挨着的猫。设了group_id,同一label下的不同实例才会被区分开。

第二,贴合轮廓但别逐根描毛。猫毛边缘本来就没有锐利边界,多边形覆盖主要轮廓就够了。要求越细,标注员和标注员之间的标准越难统一,模型学到的不是边界而是噪声。

第三,目标贴图像边缘时,多边形停在图像边界上就好,不要把顶点延伸到图外。延伸到图外会在掩码里留下一条细边,训练时既不是背景也不是前景,属于标准的脏数据。

标注过程输出的是JSON中间产物,不要手工去改目录结构,让Labelme自己管理文件。标完一批后,进入转换环节。

3.3 把JSON批量转成PNG掩码:拿来即用的转换脚本

标注完所有图,把JSON转成掩码。这是我一直在用的一个精简版转换脚本:

import json import os import numpy as np import cv2 label_map = {"background": 0, "cat": 1, "dog": 2} def json_to_mask(image_path, json_path, mask_path): img = cv2.imread(image_path) h, w = img.shape[:2] mask = np.zeros((h, w), dtype=np.uint8) with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) for shape in data["shapes"]: label = shape["label"].strip() if label not in label_map: print(f"unknown label: {label}, force skip") continue pts = np.array(shape["points"], dtype=np.int32) cv2.fillPoly(mask, [pts], label_map[label]) cv2.imwrite(mask_path, mask) return mask

逻辑很清楚:读原图拿到宽高,建一张全零掩码,遍历JSON里每个shape,用cv2.fillPoly把多边形内的像素填上对应的类别ID,最后无损写PNG。值得展开的细节有这几个:

  • cv2.imwrite写PNG默认无损,比PIL的save更可预期,不用额外指定参数。
  • 掩码数组用uint8足够,类别ID不超过255;有强迫症可以用uint16,但没必要。
  • label.strip()这行是防呆设计。标注时手滑在标签前后打了空格,label_map就匹配不上,脚本会默默跳过,最后掩码大概率缺一块。
  • mask_path要与原图严格同名,只换扩展名。我见过不少项目在转换时随意取名,到划分阶段文件名对不齐,返工成本很高。

转换完别急着开训。随便抽几张图,用np.unique检查掩码值只剩0/1/2,再用cv2.addWeighted把掩码叠加到原图上,肉眼抽查五到十张。这十分钟换来的是后面少排查好几个小时。

4. 划分训练集和测试集:一个可复现脚本把数据整理成能直接训练的目录

4.1 为什么非要用脚本划分,而不是手动拖文件

做过一遍手动整理的人都不会想再来第二次。几百张图,命名风格不统一,掩码和原图还分在两个目录,手动拖文件夹一天时间就没了,而且你今天拖完明天就忘了当时的划分比例和随机种子。脚本划分带来的两个好处是复现和可审计。复现指固定随机种子后,任何人跑同一份脚本得到完全相同的划分,训练结果可比较。可审计指划分比例、按组还是按文件切、去了训练还是测试,都有据可查,出问题能逐层回溯。

更关键的是,脚本能把“按组划分”这个规则固定下来。手动拖文件几乎不可能保证同组图片不拆散,脚本用一段循环就能严格做到。

4.2 按组划分的训练测试集脚本:同主体图像不泄漏

下面这段脚本是按组划分的标准写法。拿到手之后,要改的是路径、比例和组前缀规则。

import os import random import shutil from collections import defaultdict random.seed(42) SRC_IMAGES = "clean_images" SRC_MASKS = "converted_masks" DST = "dataset" train_ratio = 0.7 val_ratio = 0.2 # 1) 按文件名的组前缀分组,例如 group01_cat_001.jpg -> group01 groups = defaultdict(list) for name in os.listdir(SRC_IMAGES): if not name.lower().endswith((".jpg", ".jpeg", ".png")): continue group_id = name.split("_")[0] groups[group_id].append(name) # 2) 对组ID打乱,再按比例切成三份 group_ids = list(groups.keys()) random.shuffle(group_ids) n_groups = len(group_ids) n_train = int(n_groups * train_ratio) n_val = int(n_groups * val_ratio) split_map = {} for i, g in enumerate(group_ids): if i < n_train: split_map[g] = "train" elif i < n_train + n_val: split_map[g] = "val" else: split_map[g] = "test" # 3) 按组复制文件到目标目录(不是移动,原数据保留做后悔药) for group_id, files in groups.items(): split = split_map[group_id] for name in files: img_src = os.path.join(SRC_IMAGES, name) mask_src = os.path.join(SRC_MASKS, name.replace(".jpg", ".png")) img_dst = os.path.join(DST, "images", split, name) mask_dst = os.path.join(DST, "masks", split, name.replace(".jpg", ".png")) os.makedirs(os.path.dirname(img_dst), exist_ok=True) os.makedirs(os.path.dirname(mask_dst), exist_ok=True) shutil.copyfile(img_src, img_dst) shutil.copyfile(mask_src, mask_dst) print("done, groups:", n_groups, "train:", n_train, "val:", n_val, "test:", n_groups - n_train - n_val)

这段代码做了三件事,注释里已经标清:把文件名按组前缀聚成组,对组ID做shuffle并按比例切成训练、验证、测试三段,最后逐文件复制到目标目录。复制不是移动,这样原始标注文件保留完整,任何时候都能重跑划分。三个值得注意的参数:

  • random.seed(42):不写这个,脚本每次跑出的划分都不一样,后面排查问题就成了玄学。
  • train_ratio和val_ratio:脚本里test是剩下的,所以先定这两个值。比例按第二章说的,先按自己的数据量定。
  • 文件名后缀写死.jpg和.png:如果你的图有.png也有.jpg,这里就要写成根据实际后缀去凑掩码文件名,或者干脆在清洗阶段就统一后缀。我习惯把所有原图统一成.jpg、掩码统一成.png,省掉一堆麻烦。

4.3 划分完成后做三件校验:配对、尺寸、平衡

复制完不直接开训,先跑校验。第一个校验是配对,每张原图都要有同名掩码:

from PIL import Image import numpy as np import os DST = "dataset" for split in ["train", "val", "test"]: img_dir = os.path.join(DST, "images", split) mask_dir = os.path.join(DST, "masks", split) img_names = set(os.listdir(img_dir)) mask_names = set(os.listdir(mask_dir)) orphan_imgs = img_names - mask_names orphan_masks = mask_names - img_names assert len(orphan_imgs) == 0, f"{split} orphan images: {orphan_imgs}" assert len(orphan_masks) == 0, f"{split} orphan masks: {orphan_masks}" for name in img_names: img = np.array(Image.open(os.path.join(img_dir, name))) mask = np.array(Image.open(os.path.join(mask_dir, name.replace(".jpg", ".png")))) assert img.shape[:2] == mask.shape[:2], f"{name} size mismatch"

这段校验会拦下三类常见问题:原图和掩码数量对不上、命名规则不一致、以及尺寸不一致。尺寸不一致最常见的原因是某张原图或掩码在传输或转换过程中被改过,如果不校验,模型数据加载器会直接崩,崩的位置还特别不好找。

三件事里的最后一件是平衡性。把训练集的掩码统计跑一遍(用第二章的统计脚本),输出每个类别覆盖的样本数和像素占比。像素占比严重失衡的,比如背景占95%,要在训练时给前景类别加权重,这个我们在下一章展开讲。

5. 常见问题排查:猫狗分割数据集从造到用最容易翻车的五个点

做数据集的过程里,有些问题是反复出现的。下面五个是我在不同项目里都真实撞过、且周围同事也踩过同一个坑的典型,都按现象、原因、解决三个维度写。

5.1 现象:训练loss下降很快,测试集指标却忽高忽低

原因:训练集和测试集之间发生了数据泄漏。最常见的来源就是从同一段视频里抽帧,相近画面被随机分到了两侧;其次是同一个人拍摄的同一只猫在光线、角度差异极小,本质上等于同一张图的轻微扰动。

解决:回过头按“来源”重建分组信息,把所有相似镜头捆成一组,按组重新划分训练集和测试集。如果原始文件名里没有组信息而且已经丢了来源记录,那这组数据只能废掉重标一部分,别无他法。所以划分之前,先把拍摄时间、场景、主体三个信息记进文件名,永远不要等出问题后再回溯。

5.2 现象:训练时模型输出类别数总是比预期多一个

原因:掩码像素值不干净。很多标注工具导出掩码时把背景存成0、目标存成255,直接用0/1/2去对齐标签就多出一个255类。有些场景还会出现掩码边缘的过渡值,比如128。

解决:在数据加载器里做归一化,把非0的像素值统一映射成1或2。加载掩码时打印一次np.unique(mask),如果出现预期外的值,找出是哪张图,回数据源修掉。不要试图在损失函数里处理这个,治标不治本。

5.3 现象:训练集和验证集指标都不错,测试集预测图边缘糊成一片

原因:分割任务的背景像素占比通常极高,如果你用像素准确率当主指标,模型只要把整张图全预测成背景,准确率都能到90%以上,指标好看但分割结果等于零。

解决:换成IoU和Dice作为评估指标,并把预测掩码叠加原图肉眼检查。在损失函数里对前景类别加权重,常用做法是给每个类别权重设为样本像素占比的倒数,或者用FocalLoss缓解前景背景不平衡。

5.4 现象:划分脚本跑完,训练时图片读不齐,数量缺了三分之一

原因:文件名大小写不一致、图片后缀有的是.JPG有的是.jpg、掩码后缀有的是.png有的是.PNG,划分脚本里写死的后缀匹配不上,文件被静默跳过。

解决:清洗阶段就把文件名和后缀全部统一。我习惯统一转成小写、后缀统一成.jpg和.png,文件名用“{group}{category}{编号}”的规则重排。划分脚本里不要写死后缀,用Pathlib的suffix拿实际后缀去拼掩码路径,省得以后换数据还得改脚本。

5.5 现象:同样的模型换到公开数据集上效果变好,用自己的数据就崩

原因:自建数据集往往存在系统性偏差,例如所有猫狗图都来自同一处场景、同一个角度、同一个相机,模型学到了场景特征而不是猫狗本身,泛化自然差。

解决:在采集环节刻意引入多场景、多机位、多光线;如果补数据困难,退一步在训练时做更强的数据增强,随机裁剪、翻转、色彩抖动先拉满,至少让模型见过多样的几何和颜色扰动,别让场景特征成为模型真正的分类依据。

6. 验收你的猫狗分割数据集:用20张图小样本快速跑通U-Net

数据造好了,划分也完成了,别急着全量训练。一口气砸几千张图进去训十几个小时,最后发现数据有问题,成本太高。我的习惯是先抽一个小样本做冒烟测试:训练集里抽20张图、5张验证、5张测试,用最小的U-Net跑20到30轮,目标不是刷指标,而是验证整套数据管线能不能走通。

小样本跑通要盯三个信号。第一,训练loss能不能在20轮内明显下降,如果20张小图都降不下去,多半是掩码和原图没对齐,或是标签值有问题。第二,验证loss有没有跟着降,如果不降,先怀疑划分泄漏,再怀疑数据量太小。第三,把测试集的预测掩码叠加到原图上存下来,肉眼看边缘和区域,这一步比任何指标都来得直接。

算指标也至少用IoU和Dice,不要只看像素准确率。一张512x512的图,猫占不到5%像素时,全预测背景也有95%准确率,但分割任务等于完全失败。

跑通冒烟测试后,可以再走一步进阶:把这套数据的掩码转换脚本和划分脚本的随机种子、比例参数一起写进项目的README,或者直接保存成一份数据集产出的说明文件。这样训练的时候不管谁接手,第一条命令就能复现出完全一样的划分,后面排查指标波动、对比实验都省心。不少团队会忽略这件事,等换了一台机器、重新划分一次数据,所有对比实验就失去了意义。

如果你打算把这套数据进一步拿去接YOLOv8-seg这类分割框架,思路也是一样的:先把掩码转成对应格式的标注文件,再按组做同样的划分,冒烟测试过了再全量。换框架换的是标注格式,数据划分的准则不变。

最后说一句我的习惯:每次造完数据集,我都会在跑完第一轮冒烟测试后,写一段三行的记录,包含总图数、掩码格式、划分比例和发现的问题。这个习惯不花几分钟,但下次再面对图像分割任务时,能少走很多弯路。希望这套流程对你也有帮助。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询