ARTICLE DETAIL

资讯详情

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

宠物图像语义分割实战:7000张数据集与U-Net训练框架

宠物图像语义分割实战:7000张数据集与U-Net训练框架 简介这是一套面向图像分割任务的大型宠物语义分割数据集适合计算机视觉学习者、算法工程师用于分割模型的数据准备与效果验证。压缩包共2000个文件以PNG图像与对应掩码为主体另含TXT类别说明和PY可视化脚本整包约765.67MB包内按images与masks目录组织样本便于在主流分割框架中直接读取。数据已完成训练集与验证集划分标注覆盖背景、宠物、边缘三类像素级标签可直接支撑U-Net、DeepLab等常见分割网络的训练与评估可视化脚本可随机抽取样本将原始图像、GT标签图与GT叠加蒙版图并行展示便于快速检查标注质量。目前已有48人浏览/学习适合需要现成标注数据开展宠物分割实验的初学者和进阶研究者也能节省数据采集与标注环节的时间成本。1. 图像分割数据集先想清楚这 7000 张图能替你解决什么在图像分割项目里真正劝退人的往往不是模型结构而是数据集自己用标注工具画一张带宠物的图毛发边缘要描半天画 100 张就没了耐心。这个大型宠物图像语义分割数据集直接把约 7000 张带像素级标签的宠物图片给你省掉的是最耗时、最影响模型上限的数据准备环节。它能解决三个最实际的诉求验证语义分割算法、做宠物抠图相关的前置模型以及拿一份现成数据测试数据增强和迁移学习方案。适合拿来做算法对比实验的学生也适合给宠物类产品做背景替换、美化功能的一线工程师。下面按“数据集长什么样 → 怎么训练 → 怎么评估 → 有哪些坑”往下拆。2. 数据集的构成与标签体系先摸清这 7000 张图的家底2.1 图像来源与样本分布分布决定训练上限拿到数据集的第一步不是直接开训而是先花半小时把数据摸清楚。约 7000 张的规模听上去不少但对语义分割来说只是入门配置一张 512×512 的输入图有超过 26 万个像素模型要在这么多像素上学会区分猫、狗和背景样本量稍一不足就会过拟合。很多宠物数据集的图片来源是日常拍摄和网络抓取的混合体室内室外、白天夜晚都有猫狗占比并不均衡背景更是五花八门。这种分布决定了你后面要是不做任何处理直接训练最终 mIoU 的上限就是被这些占比小的类别和场景拉低的。先把类别像素占比统计出来才知道损失函数里该不该加权import numpy as np from PIL import Image from pathlib import Path # class_map 示例0 是背景1 是猫2 是狗颜色需要和掩码保持一致 class_map { 0: (0, 0, 0), 1: (255, 0, 0), 2: (0, 0, 255), } mask_dir Path(data/masks) class_pixels {} total_pixels 0 for i, mask_path in enumerate(mask_dir.glob(*.png)): mask np.array(Image.open(mask_path).convert(RGB)) for class_id, color in class_map.items(): class_mask (mask np.array(color)).all(axis-1) class_pixels[class_id] class_pixels.get(class_id, 0) int(class_mask.sum()) total_pixels mask.shape[0] * mask.shape[1] if i % 500 0: print(fprocessed {i} masks)逻辑说明遍历每张掩码把 RGB 颜色值逐像素匹配到类别 ID累加每个类别的像素总数最后用各类像素数除以 total_pixels 得到占比。匹配时用np.all(mask color, axis-1)才能同时比较 RGB 三个通道只比较单通道会把颜色相近的不同类别混到一起。统计完通常会发现背景占 70% 以上猫和狗的像素占比可能相差两到三倍。如果在这个数据集上做的是“背景 / 猫 / 狗”三分类类别权重建议按像素占比倒数来设如果只做前景背景二分类问题会少一些但边缘依然难。另一个容易被忽略的步骤是数据清洗。我一般会先删掉几类图掩码中宠物像素占比过小的图例如小于整张图 0.5%、没有宠物主体的空标签图、以及无法正常打开的损坏文件。约 7000 张里挑出几十张问题数据很正常它们不会让 mIoU 掉太多但会让训练过程出现莫名其妙的 loss 抖动。清洗逻辑很简单统计每张掩码的类别像素数量低于阈值的单独列出来人工看一眼再决定删除还是保留。2.2 标签格式与类别设计从 RGB 掩码到单通道索引宠物语义分割数据集的标签格式不统一常见的有两种一种是单通道索引图像素值直接是类别 ID常见于 P 模式 PNG另一种是 RGB 颜色编码图每个类别固定对应一种颜色。拿到数据集第一件事就是判断掩码的 mode否则后续转换很容易埋雷。from PIL import Image mask Image.open(data/masks/0001.png) print(mask.mode, mask.getextrema())P 模式输出modePgetextrema 返回调色板索引范围RGB 模式输出modeRGBgetextrema 返回每个通道的最小最大值。把这一行放到数据加载脚本里跑一遍能避免后面所有颜色映射相关的错误。如果是 RGB 颜色编码训练前必须把它转成单通道索引类别 ID 从 0 开始连续递增。转换时注意三点第一类别 ID 一定要连续模型输出通道数等于类别数中间跳一个 ID 会导致训练时报 shape 不匹配第二未标注区域建议约定为 255在损失函数里用 ignore_index 跳过不要把它当成一个类别去训练第三颜色表要跟掩码完全一致有的数据集里同一个类别可能有两种相近的颜色需要先做颜色量化。import numpy as np from PIL import Image def rgb_mask_to_index(mask_path, class_map): mask np.array(Image.open(mask_path).convert(RGB)) h, w mask.shape[:2] index np.zeros((h, w), dtypenp.uint8) for class_id, color in class_map.items(): index[np.all(mask np.array(color), axis-1)] class_id return index转换代码里 class_map 的 key 是最终类别 IDvalue 是 RGB 颜色for 循环里np.all(mask color, axis-1)逐像素匹配把匹配到的位置填成对应类别 ID。转换后做一次合法性检查统计索引图里最大类别 ID 是否小于类别数如果出现超范围 ID说明标签体系里混入了未处理的类别要回查类别定义而不是急着调模型。还有一个常见误区是直接对 RGB 掩码做灰度化再当标签用。灰度化之后不同类别的灰度值可能相同比如 (255, 0, 0) 和 (0, 0, 255) 转成灰度后都是 76 附近模型学到的是错误映射。判断方式就是上面那段 mode 检查RGB 模式一律走颜色字典P 模式走调色板。2.3 数据划分按 8:1:1 拆分并避免同一只宠物泄漏数据划分直接决定你调参时看到的是真实水平还是虚高的分数。约 7000 张不算大如果只用 8:2 切成两份验证集只有 1400 张随机性太大调几轮学习率就会发现结果忽高忽低。我一般按 8:1:1 拆成训练、验证、测试三份测试集只在最终评估时用一次。data/ train/ images/ 001.jpg 002.jpg ... masks/ 001.png 002.png ... val/ images/ 011.jpg 012.jpg ... masks/ 011.png 012.png ... test/ images/ 021.jpg 022.jpg ... masks/ 021.png 022.png ...划分之前还有一个更重要的检查同一只宠物不能既出现在训练集又出现在验证集或测试集。宠物数据集的图片往往来自同一只动物多次采集同一只猫换个角度拍了七八张如果按文件名随机切分模型等于提前见过了这只猫验证 mIoU 虚高五到八个点都很正常。判断有没有个体泄漏的办法是看文件名前缀很多数据集命名带主体 ID比如cat_001_02.jpg里的前两段就是个体编号。如果有这种规则先按主体 ID 分组再划分import random from pathlib import Path random.seed(42) samples sorted(p.stem for p in Path(data/images).glob(*.jpg)) random.shuffle(samples) n len(samples) train samples[:int(n * 0.8)] val samples[int(n * 0.8):int(n * 0.9)] test samples[int(n * 0.9):]固定 seed 的目的是让每次实验可复现别人也能按同样的逻辑对比。如果文件名能看到主体 ID不要直接这样切先 group by 前缀再划分。文件组织上我推荐用 train.txt、val.txt 清单文件每行一个样本名DataLoader 按清单读取省磁盘也不容易因为复制漏掉配对。划分完成后记录每个集合的类别像素占比确认三份数据的类别分布大致一致避免训练集里猫多、测试集里狗多这种失衡。另外看一眼光照分布如果测试集全部是室内白天的图验证集最好也不要全选户外夜晚否则调参时你会误以为模型泛化很好。语义分割最怕的就是验证集分布和真实使用场景不一致这一点比模型选型更容易在后期翻车。3. 把数据集喂给语义分割模型U-Net 最小训练闭环3.1 数据加载器图片与掩码同步读取与预处理训练语义分割模型的开端是写一个正确的 Dataset。最容易出错的地方不在模型而在图片和掩码的同步问题resize 不一致、随机增强不同步、掩码用了会插值的算法。下面这个 Dataset 类是 U-Net 训练的最小骨架import torch from torch.utils.data import Dataset from PIL import Image import numpy as np from pathlib import Path class PetSegDataset(Dataset): def __init__(self, image_dir, mask_dir, class_map, image_size(512, 512)): self.image_size image_size self.class_map class_map self.ids sorted(p.stem for p in Path(image_dir).glob(*.jpg)) self.image_dir Path(image_dir) self.mask_dir Path(mask_dir) def __len__(self): return len(self.ids) def __getitem__(self, idx): sid self.ids[idx] image Image.open(self.image_dir / f{sid}.jpg).convert(RGB) mask Image.open(self.mask_dir / f{sid}.png).convert(RGB) image image.resize(self.image_size, Image.BILINEAR) mask mask.resize(self.image_size, Image.NEAREST) image np.array(image).astype(np.float32) / 127.5 - 1.0 mask rgb_mask_to_index(np.array(mask), self.class_map) image torch.from_numpy(image.transpose(2, 0, 1)) mask torch.from_numpy(mask).long() return image, mask核心约定有两条图像用双线性缩放掩码用最近邻缩放掩码在__getitem__里转成单通道索引并转成 long tensor。rgb_mask_to_index沿用 2.2 里的转换函数class_map 的传入让 Dataset 和标签体系解耦换数据集时只要改颜色字典。返回的 image 是 float32 的 [-1, 1] tensormask 是 long tensorCrossEntropy 要求标签必须是 long 且形状为 (H, W)。提示掩码任何一步都不能用双线性或立方插值否则类别边界会出现“中间类别”损失函数直接崩。这里没写数据增强是为了保持代码逻辑直观实际训练时把随机水平翻转、随机裁剪、颜色抖动放在同步块里具体见 5.5。加载时如果发现图片是 RGBA 模式先convert(RGB)否则后面 batch 时会因为 4 通道和 3 通道不一致报错。输入尺寸我习惯固定成 512×512这是显存和精度的折中点如果显存只有 8G降到 384 也没问题但不要随便用非方形尺寸U-Net 下采样到最低层时非对称尺寸会带来额外 padding 问题。3.2 模型选型与损失函数为什么我从 U-Net 起步语义分割算法选型要匹配数据规模。FCN 是较早把全连接层替换成卷积的全卷积语义分割模型结构简单但上采样太粗糙在宠物这种细毛边缘场景里表现不好。U-Net 的编码器-解码器加跳接结构对小数据集非常友好跳接把低层细节直接送到 decoder弥补了上采样丢失的边界信息。约 7000 张的规模下U-Net 是门槛最低、最容易训稳的选择。显存够且想把分数推高时再换 DeepLabV3 这类带空洞卷积和 ASPP 的结构多尺度能力更强但对显存和调参耐心要求也更高。损失函数上宠物分割最直接的痛点是背景像素占绝对多数。直接 CrossEntropy 能训但模型很容易用“全预测背景”来骗过高准确率所以我用加权交叉熵加 Dice 的混合损失import torch import torch.nn as nn import torch.nn.functional as F class WeightedCELoss(nn.Module): def __init__(self, class_weights): super().__init__() self.register_buffer(weight, torch.tensor(class_weights, dtypetorch.float32)) def forward(self, logits, labels): return F.cross_entropy(logits, labels, weightself.weight, ignore_index255) class DiceLoss(nn.Module): def forward(self, logits, labels): probs F.softmax(logits, dim1) one_hot F.one_hot(labels, num_classesprobs.shape[1]).permute(0, 3, 1, 2) smooth 1.0 intersection (probs * one_hot).sum(dim(2, 3)) union probs.sum(dim(2, 3)) one_hot.sum(dim(2, 3)) return 1 - ((2 * intersection smooth) / (union smooth)).mean() # loss 0.5 * WeightedCELoss([0.3, 1.0, 1.5])(logits, labels) \ # 0.5 * DiceLoss()(logits, labels)权重来自 2.1 统计的像素占比倒数比如背景 0.3、猫 1.0、狗 1.5具体值按实际分布调。Dice 损失对前景小物体更敏感两个损失按 0.5 和 0.5 相加是我常用的起点。ignore_index255用来跳过未标注区域这在宠物数据集里很常见。需要说明的是one_hot 转换会占额外显存512×512×3 类还好如果类别数超过 10建议只在计算 Dice 时把 one_hot 下采样到低分辨率再做。3.3 训练参数batch size、学习率与 epoch 怎么调模型和损失定好后超参决定能不能收敛。我按 12G 显存、512×512 输入、U-Net 的配置给出下面的起点显存小就优先降 batch size其次降输入分辨率到 384。batch size 太小会让 BatchNorm 统计量抖动所以即便显存小也不要低于 4。参数推荐起点说明image_size512×512八的倍数方便下采样batch_size812G 显存稳妥值优化器AdamWweight_decay 1e-4初始学习率1e-4预训练 backbone 解冻后可降到 5e-5学习率调度poly, power0.9也可以每 20 epoch 乘 0.5epochs80120配合早停 patience 15混合精度AMP显存减半速度提升明显poly 调度的公式是lr base_lr * (1 - iter / total_iter) ** 0.9语义分割里用它的原因是训练后期希望学习率缓慢降到一个极小值让 decoder 的细粒度输出稳定下来。用 step 调度也能训但需要多试几个衰减节点poly 省心一点。使用 ImageNet 预训练权重是约 7000 张这个规模下非常值得做的迁移学习手段。常见做法是取 imagenet1k 上训练好的分类骨干ResNet34 或 ResNet50作为 U-Net 编码器的初始权重前 10 个 epoch 冻结 backbone 只训练 decoder之后再解冻整个网络用更小学习率微调for name, param in model.named_parameters(): if encoder in name and norm not in name: param.requires_grad False冻结操作的核心是遍历模型参数把 encoder 里除了 norm 层的参数requires_grad设为 False。norm 层BatchNorm 的 gamma 和 beta无论如何都要训练否则 BN 统计量和冻结参数不匹配训练后期 loss 容易出现锯齿。前 10 个 epoch 里 decoder 会先学会基础轮廓解冻后整个网络的收敛速度会快很多这也是小数据集上最稳的训练顺序。4. 评估语义分割mIoU 之外再看这几张可视化4.1 指标计算从混淆矩阵到逐类 IoU训练跑到一半最关心的问题就是模型到底变好了没有语义分割的标准指标是 mIoU但只看最终数值会让你漏掉大量问题。正确做法是先构建混淆矩阵再算逐类 IoU最后取平均。构建混淆矩阵时注意过滤掉 ignore_index 的像素否则未标注区域会污染结果。宠物分割里背景占大头像素准确率可以轻松到 0.9 以上但猫狗的 IoU 可能只有 0.5 上下所以打印指标时要逐类打印不要只打一个 mIoU。import numpy as np def build_confusion_matrix(labels, preds, num_classes): labels labels.flatten() preds preds.flatten() mask (labels 0) (labels num_classes) return np.bincount( labels[mask] * num_classes preds[mask], minlengthnum_classes * num_classes ).reshape(num_classes, num_classes) def compute_iou(confusion_matrix): intersection np.diag(confusion_matrix) union confusion_matrix.sum(axis0) confusion_matrix.sum(axis1) - intersection per_class_iou intersection / (union 1e-6) return per_class_iou逻辑说明labels 和 preds 的形状都是 (H, W)flatten 后通过labels * num_classes preds把二维混淆矩阵的坐标映射成一维索引再 bincount 回矩阵。这个写法比双重循环快很多验证集几千张图也能在几秒内算完。per_class_iou 里加 1e-6 防除零因为某些小类别可能没出现在样本里。mIoU 是所有类别的平均值遇到样本极不均衡时建议同时输出 weighted IoU按各类像素占比加权那个值更贴近产品实际体验。4.2 错误分析把 IoU 最低的失败样本挑出来看指标只能告诉你模型多差不能告诉你差在哪。我习惯在验证阶段把每个样本的逐类 IoU 存下来然后把猫或狗 IoU 最低的 20 张图做成拼版左边原图、中间 GT、右边预测错的地方一眼就能看出来。import numpy as np from PIL import Image def save_overlay(image, label, pred, path): # label_color 和 pred_color 分别是 GT、预测类别映射后的 RGB 颜色图 overlay image * 0.4 label_color * 0.3 pred_color * 0.3 Image.fromarray((overlay * 255).astype(np.uint8)).save(path)可视化代码里用半透明叠加而不是直接拼接因为把原图和预测并排时人眼对微小错位的敏感性远低于半透明叠上去的色差。重合区域会自然变成黄色错分区域一眼可见。宠物分割里最常见的失败模式是深色宠物在暗光环境下的边缘和背景融为一体其次是细尾巴、耳朵尖和胡须这类长尾结构被模型直接丢弃。如果低 IoU 样本集中在某个场景说明不是模型问题是训练数据里这个场景太少这时候加增强比换模型更有效。保存下来的失败样本还可以当标注质量的检查材料。有时 mIoU 低不是模型差而是 GT 本身就画错了耳朵尖标进去或者尾巴缺了一截。这种情况要先修标签再骂模型否则你对着一个错标签调三个月网络也是白搭。最后补充验证策略。约 7000 张这个规模下训练时用 512推理时可以试试把图像放大到 768或者做水平翻转的 TTA通常能提升 12 个点的 mIoU代价是推理时间翻几倍。验证阶段算指标比较慢常见做法是每 5 个 epoch 跑一次完整验证而不是每 epoch 都跑mIoU 曲线平滑度也更好。最终报告里要写清楚输入尺寸、是否做 TTA、是否用 CRF 后处理否则复现你对不上数字。5. 宠物分割的常见坑与排查五条高频踩坑记录5.1 PNG 掩码读出来全黑或颜色对不上现象训练 loss 正常可视化掩码时全黑或者预测结果和 GT 完全错位。原因掩码是 P 模式调色板 PNG直接用convert(RGB)或convert(L)读出来以后颜色顺序和数据集文档里的颜色表对不上P 模式的调色板索引到 RGB 需要查表直接当灰度图读就是黑乎乎一片。解决先打印 mask.modeP 模式用 getpalette() 取调色板按调色板索引转类别RGB 模式用颜色字典逐像素匹配。一个小技巧是随机抽三张掩码把每个类别的像素取出来看平均 RGB和 color map 对比后再写进 class_map。im Image.open(mask_path) if im.mode P: palette im.getpalette() # 每 3 个值是一个 RGB 颜色 elif im.mode RGB: # 按颜色字典逐像素匹配 pass5.2 背景占比太大模型直接摆烂预测全背景现象验证时总 mIoU 有 0.6 以上但猫和狗的逐类 IoU 不到 0.1。原因CrossEntropyLoss 被背景 70% 以上的像素主导模型发现全预测背景就能拿到不低的 loss 下降直接放弃前景。另一个隐藏问题是未标注区域没设 ignore_index255模型把不该学的像素也学进去了。解决换加权 CE 或 CEDice 混合损失类别权重按像素占比倒数设置训练脚本里同时输出 per_class IoU不让总 mIoU 掩盖问题。检查 loss 曲线的话背景类占比高时 loss 下降很快但前景类 IoU 迟迟不动基本就是这个坑。5.3 毛发边缘和细尾很容易被割断现象预测图里胡须消失、尾巴中断、耳朵边缘锯齿严重。原因输入分辨率低、掩码 resize 用了插值、模型缺少多尺度特征三者叠加导致细长结构被吞掉。耳朵尖和尾巴尖只占几个像素下采样一次就可能消失。解决把输入提高到 512 以上DeepLabV3 的 ASPP 或 U-Net 的深监督都会有帮助推理时用多尺度拼接把原图、缩放 0.8 倍和 1.2 倍的预测取平均边缘后处理上 CRF 能稍微修整但治标不治本。如果项目对边缘要求高可以在损失里加一个边缘感知项对 GT 的 Sobel 梯度图区域加大权重。5.4 图片尺寸不统一导致 DataLoader 报错现象训练跑到一半RuntimeError: stack expects each tensor to be equal size。原因数据里有几张宽高不一致的图甚至包含 4 通道 RGBADataset 里没统一尺寸就进了 batch。宠物数据集常见上传图片尺寸各异文件名还不一定按尺寸分组。解决__getitem__里强制 resize 到固定尺寸所有输入先convert(RGB)再写一行检查所有 mask 的 size 集合确认没有漏网之鱼sizes {Image.open(p).size for p in mask_dir.glob(*.png)} print(sizes)如果 size 集合里出现一个奇怪的值单独处理那几张不要直接删先看是掩码本身画错了还是文件损坏。用 collate_fn 做 padding 也能强行跑起来但 padding 区域最后预测结果会被忽略指标会偏乐观工程上不推荐。5.5 数据增强后图像和掩码错位现象加了随机翻转和裁剪后验证 mIoU 不升反降loss 震荡加剧。原因图像和掩码各自执行了随机变换随机种子不一致或者掩码 resize 用了双线性。翻转后猫跑到图左边掩码还留在右边模型等于在学习乱码。解决图像和掩码共享同一个随机种子颜色抖动只作用于图像。同步变换的代码框架是这样seed torch.randint(0, 2 ** 32, ()) torch.manual_seed(seed) image transform(image) torch.manual_seed(seed) mask transform(mask)水平翻转、随机裁剪、缩放都要执行这个双 seed 操作颜色抖动只在图像部分做掩码绝对不能做颜色变换。把同步 transform 封装成一个函数避免在 Dataset 里反复复制粘贴增强逻辑变更时也只改一处。6. 把 7000 张图用出 7 万张的效果迁移学习与 CutMix这个规模的数据集换一个更复杂的模型往往不如在数据和训练策略上下工夫。第一个必做项是迁移学习U-Net 编码器用 ImageNet 预训练权重初始化前 10 个 epoch 冻结 encoder只让 decoder 收敛之后再解冻全网络、把学习率降到 5e-5 微调。以前总觉得 7000 张够多随机初始化也能训后来发现 train mIoU 0.97、val mIoU 0.72 的过拟合差距就是从换预训练权重开始缩小的。第二个值得做的是 CutMix 风格的增强。语义分割里不能直接把两张图拼在一起标签图必须同步拼。做法是随机从 batch 里取另一张图生成一个矩形区域把当前图和标签对应区域替换成另一张图的区域。这个增强对宠物分割特别有效让模型不能依赖背景来判断类别能强迫它学习猫狗形体本身。import numpy as np def cutmix(image, label, image2, label2, region(0.25, 0.5)): h, w image.shape[1:] bh int(h * np.random.uniform(*region)) bw int(w * np.random.uniform(*region)) y0 np.random.randint(0, h - bh) x0 np.random.randint(0, w - bw) image[:, y0:y0bh, x0:x0bw] image2[:, y0:y0bh, x0:x0bw] label[:, y0:y0bh, x0:x0bw] label2[:, y0:y0bh, x0:x0bw] return image, label矩形区域面积控制在整张图的 0.25 到 0.5 倍之间太大让模型困惑太小起不到强迫学习的作用。配合 CEDice 混合损失用比单独用任何一种都稳。选择 CutMix 的样本时尽量选类别差异大的组合比如猫和狗拼一起效果比猫拼猫好。最后是一条老教训用随机初始化 U-Net 直接训练这 7000 张图训练曲线看起来一切正常翻车点全在验证阶段把预训练、同步增强、CutMix 三者叠加后val mIoU 才稳定到能落地的水平。数据增强和迁移学习不是玄学是这个规模下最值得先花时间的两个方向。希望帮到你。本文还有配套的精品资源点击获取
返回列表