ARTICLE DETAIL

资讯详情

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

苹果缺陷语义分割数据集实战:4000张图5类标签,从可视化到UNet训练全链路

苹果缺陷语义分割数据集实战:4000张图5类标签,从可视化到UNet训练全链路 简介本资源为苹果缺陷图像语义分割数据集面向从事图像分割算法研究、深度学习模型训练与缺陷检测应用的学生及开发者可用于语义分割网络的训练、验证与效果对比。数据集已按训练集与测试集划分完毕训练集约3000张图片及对应mask测试集约1200张共约4000张样本涵盖健康、病害区域等5类分割标签具体类别可参考classes文件。压缩包共2000个文件以1998个png图像与掩膜为主另含1个txt类别说明和1个py可视化脚本整体约73.28MB。可视化脚本可随机抽取一张图片展示原始图像、GT图像及GT在原图上的蒙板效果并保存至当前目录便于快速核验标注质量。目前已有180人学习适合作为分割模型训练与改进实验的配套数据。1. 苹果缺陷语义分割数据集4000 张图、5 类标签拿到手先跑通这条链路做工业质检的算法同学大概率都遇到过这个场景产线相机架好了光源调完了老板问「缺陷分割模型什么时候能出第一版效果」而你手里一张标注数据都没有。公开数据集里找苹果表面缺陷的语义分割资源要么类别对不上要么只有分类标签没有像素级 mask要么就是几十张图的玩具集训出来的模型一上产线就翻车。这份苹果缺陷图像语义分割数据集约 4000 张图像加对应 mask已经按训练集 3000 张左右、测试集 1200 张左右切分好目录结构是标准的 images masks 双目录5 类分割标签配套一个可视化脚本随机抽一张图把原图、GT 图和 GT 蒙板叠图展示出来。它适合三类人想快速验证 UNet / DeepLabV3 / SwinUnet 这类语义分割网络在真实工业缺陷场景表现的工程师需要一份带像素级标注的多类别缺陷数据做课程设计或毕设的学生以及想拿它当模板去复现自己产线缺陷分割流程的从业者。下面按「数据长什么样 → 怎么接进训练框架 → 坑在哪 → 怎么验证」的顺序拆开讲。2. 数据组织与标签体系从文件名到 5 类 mask 的映射逻辑2.1 目录结构与文件命名规律这份数据集最省心的地方是切分已经做好了不用自己写 split 脚本。典型目录长这样apple_defect_seg/ ├── train/ │ ├── images/ # 3000 张左右原图 │ └── masks/ # 与 images 一一对应的 mask ├── test/ │ ├── images/ # 1200 张左右原图 │ └── masks/ ├── classes.txt # 5 类标签定义 └── visualize.py # 可视化脚本从项目正文给出的文件名样本能看出命名规律比如21_01_03_19_15_00_Canon_top_all_on_msk_6.png和21_01_03_19_15_00_Canon_top_side_only_msk_6.png。前缀是时间戳年_月_日_时_分_秒中间是设备标识Canon_top再往后是拍摄模式all_on或side_only最后msk_6是 mask 相关标识。images 和 masks 里的文件同名只是所在目录不同这是语义分割数据集最常见的配对方式。写 DataLoader 时直接按文件名 stem 去 masks 目录找同名文件即可不需要额外的映射表。提示拿到数据后先跑一遍ls train/images | wc -l和ls train/masks | wc -l两个数字必须相等。不等就说明有图缺 mask 或者有 mask 缺图后面训练会直接报错。2.2 5 类标签的读取与像素值约定classes.txt 里定义了 5 个类别摘要里明确提到包含「健康、病害区域等」具体以 classes 文件为准。语义分割的 mask 是单通道灰度图或索引图每个像素值对应一个类别 ID。常见约定是 0 为背景、1 到 N 为各个缺陷类别但不同标注工具导出的 mask 像素值不一定从 0 连续排。所以第一步不是急着训模型而是先把 mask 的像素分布统计出来import numpy as np from PIL import Image from pathlib import Path mask_dir Path(apple_defect_seg/train/masks) # 统计所有 mask 里出现过的像素值 unique_vals set() for p in mask_dir.glob(*.png): arr np.array(Image.open(p)) unique_vals.update(np.unique(arr).tolist()) print(mask 中出现的像素值:, sorted(unique_vals))这段代码的逻辑是遍历训练集所有 mask把每张图里出现过的像素值收集到一个集合里。如果输出是[0, 1, 2, 3, 4, 5]这种连续值说明类别 ID 是连续的可以直接用如果出现[0, 128, 255]这种跳变值就需要建一个映射字典把原始像素值重映射到 0 到 4 的连续标签否则交叉熵损失函数的类别数会对不上。参数上唯一要注意的是Image.open默认按原模式读取mask 一般是L模式8 位灰度如果读出来是P模式调色板要先.convert(L)再转 numpy。2.3 可视化脚本怎么用、看什么数据集自带的可视化脚本是验证标注质量最快的手段。它的逻辑是随机抽一张图把原图、GT 图、GT 在原图上的蒙板叠加三张图并排展示同时保存到当前目录。跑之前确认脚本里的路径指向你的实际解压目录cd apple_defect_seg python visualize.py跑完当前目录会多出几张 png。重点看三件事一是 GT 图的类别区域是否和原图缺陷位置对得上如果 mask 整体偏移说明标注时图像和 mask 尺寸不一致二是蒙板叠加图的透明度是否合适太透明看不清边界太不透明盖住原图纹理三是随机多跑几次看不同拍摄模式all_on 和 side_only下的标注一致性。我一般会连续跑十次把十张叠加图拼成一张大图扫一眼能快速发现个别标注跑偏的样本。3. 接进 UNet / DeepLabV3 训练Dataset 写法与参数配置3.1 自定义 Dataset 的完整写法这份数据是标准双目录结构写 PyTorch 的 Dataset 很直接。下面是一个能直接用的版本包含图像和 mask 的同步增强import torch from torch.utils.data import Dataset from PIL import Image from pathlib import Path import numpy as np import albumentations as A from albumentations.pytorch import ToTensorV2 class AppleDefectDataset(Dataset): def __init__(self, root, splittrain, img_size512): self.img_dir Path(root) / split / images self.mask_dir Path(root) / split / masks self.names sorted([p.stem for p in self.img_dir.glob(*.png)]) # 训练集做增强验证/测试集只做 resize 和归一化 if split train: self.tf A.Compose([ A.Resize(img_size, img_size), A.HorizontalFlip(p0.5), A.RandomBrightnessContrast(p0.3), A.Normalize(mean(0.485, 0.456, 0.406), std(0.229, 0.224, 0.225)), ToTensorV2(), ]) else: self.tf A.Compose([ A.Resize(img_size, img_size), A.Normalize(mean(0.485, 0.456, 0.406), std(0.229, 0.224, 0.225)), ToTensorV2(), ]) def __len__(self): return len(self.names) def __getitem__(self, idx): name self.names[idx] img np.array(Image.open(self.img_dir / f{name}.png).convert(RGB)) mask np.array(Image.open(self.mask_dir / f{name}.png).convert(L)) out self.tf(imageimg, maskmask) return out[image], out[mask].long()逻辑上分三块初始化时把 images 目录下所有文件名 stem 收集成列表保证图像和 mask 通过同一个 stem 配对增强部分用 albumentations 的 Compose图像和 mask 会同步做几何变换这点比手写 torchvision transform 安全不会出现图像翻转了 mask 没翻的情况__getitem__里 mask 用L模式读成单通道最后转long类型因为交叉熵损失要求标签是 int64。参数上img_size我一般设 512苹果缺陷区域相对整图占比不算特别小512 能保留足够边界细节显存吃紧就降到 384。归一化用的是 ImageNet 均值方差如果你从零训可以用数据集自身统计值但用预训练权重就必须保持这套。3.2 训练参数与损失函数选择5 类分割属于多类别语义分割损失函数首选交叉熵如果类别像素占比悬殊再加 Dice 或 Focal 做辅助。下面是一个最小训练循环的关键参数import torch.nn as nn from torch.utils.data import DataLoader dataset AppleDefectDataset(apple_defect_seg, splittrain, img_size512) loader DataLoader(dataset, batch_size4, shuffleTrue, num_workers4) model UNet(in_channels3, num_classes5).cuda() # 以 UNet 为例 criterion nn.CrossEntropyLoss(ignore_index255) # 255 为忽略像素 optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max50) for epoch in range(50): model.train() for img, mask in loader: img, mask img.cuda(), mask.cuda() pred model(img) loss criterion(pred, mask) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step()num_classes5对应 5 类标签ignore_index255是语义分割里的常见约定标注边界模糊的像素会被标成 255训练时跳过不计算损失。batch_size 设 4 是 512 分辨率下 8G 显存的保守值显存够可以往上加。学习率 1e-4 配 AdamW 是分割任务比较稳的起点余弦退火让后期学习率降下来收敛更细。如果你用 DeepLabV3 或 SwinUnet把 model 那一行换掉即可输入输出接口是一致的。3.3 从训练日志判断数据是否接对了训练跑起来后别只看 loss 降不降前几个 epoch 重点看两个信号。第一初始 loss 应该在ln(5) ≈ 1.61附近如果初始 loss 是 0.几或者 3 点几说明类别数或者 mask 像素值映射有问题。第二训几个 epoch 后把验证集预测结果可视化出来如果预测全是同一类大概率是类别极度不平衡或者 mask 读取时像素值没重映射。我一般会在第一个 epoch 结束后就抽一张验证图跑推理看预测 mask 的类别分布比等 loss 曲线更早发现问题。4. 避坑与排查这份数据集最容易翻车的五个地方4.1 现象训练报错「Target out of bounds」原因mask 里存在大于等于 num_classes 的像素值比如类别 ID 是 1 到 5 但 num_classes 设成了 5最大 ID 5 越界了。或者 mask 里有 255 忽略像素但损失函数没设 ignore_index。解决先跑 2.2 节的像素统计脚本确认最大像素值。如果类别 ID 从 1 开始要么把 num_classes 设成 6要么在 Dataset 里把像素值减 1 重映射到 0 到 4。有 255 的话损失函数必须带ignore_index255。4.2 现象图像和 mask 尺寸对不上增强时报 shape 错误原因部分原图和 mask 分辨率不一致或者 mask 是调色板模式读出来通道数不对。解决在 Dataset 里读 mask 时强制.convert(L)读图像强制.convert(RGB)。albumentations 的 Resize 会同时作用在 image 和 mask 上只要送进去之前两者空间尺寸一致就不会报错。如果原始尺寸就不一致先用 PIL 把 mask resize 到和图像同尺寸再送增强。4.3 现象验证集指标虚高但实际推理一塌糊涂原因训练集和测试集如果来自同一批次拍摄光照和背景高度相似模型学到的是背景捷径而不是缺陷特征。这份数据里 all_on 和 side_only 两种拍摄模式如果分布不均测试集恰好偏向某一种指标就会虚高。解决按拍摄模式分层抽样检查训练集和测试集的模式分布如果偏差大手动重新划分。验证时除了看 mIoU一定要把预测 mask 叠加到原图上肉眼过一遍别只信数字。4.4 现象可视化脚本跑出来蒙板全黑或全白原因mask 像素值范围是 0 到 5直接当灰度图显示时对比度极低肉眼几乎看不出类别差异。解决可视化前先把 mask 做伪彩色映射或者乘以一个系数放大像素值再显示。叠加蒙板时用cv2.addWeighted控制透明度原图权重 0.6、蒙板权重 0.4 是比较舒服的比例。4.5 现象DataLoader 的 num_workers 设大了反而变慢原因PNG 解码是 CPU 密集型操作worker 数超过物理核心数会频繁上下文切换。另外如果数据放在机械硬盘上多 worker 并发读会互相抢 IO。解决num_workers 从 4 开始试观察 GPU 利用率如果 GPU 利用率上不去再加。数据放 SSD 上或者提前把 PNG 转成 numpy 的 npy 格式减少解码开销。5. 进阶技巧用这套数据验证分割模型改进是否真的有效拿到一份能跑通的数据集只是起点真正体现价值的是用它做对照实验。我一般会固定三件事固定训练集和测试集划分不动固定输入分辨率 512固定随机种子。然后在这个基础上换模型结构或者损失函数看 mIoU 和边界区域的指标变化。具体做法是写一个评估脚本除了算整体 mIoU再单独算缺陷区域边界的 F1。边界区域的定义可以用 mask 做形态学膨胀减去腐蚀得到一圈边界带只在这个带上算指标。很多模型整体 mIoU 涨了但边界反而变差工业质检里边界精度往往比整体精度更重要。import cv2 import numpy as np def boundary_f1(pred, gt, kernel_size5): kernel np.ones((kernel_size, kernel_size), np.uint8) gt_bound cv2.dilate(gt, kernel) - cv2.erode(gt, kernel) pred_bound cv2.dilate(pred, kernel) - cv2.erode(pred, kernel) tp np.logical_and(pred_bound, gt_bound).sum() fp np.logical_and(pred_bound, ~gt_bound.astype(bool)).sum() fn np.logical_and(~pred_bound.astype(bool), gt_bound).sum() precision tp / (tp fp 1e-6) recall tp / (tp fn 1e-6) return 2 * precision * recall / (precision recall 1e-6)这段代码先用膨胀减腐蚀提取边界带再在边界带上算 F1。kernel_size 控制边界带宽度5 对应大约 2 像素宽的边界可以根据缺陷最小尺寸调整。跑对照实验时每次只改一个变量比如只把 UNet 的编码器从 ResNet34 换成 ResNet50其他全不动跑三次取平均这样得出的结论才站得住。还有一个习惯是每次实验都把配置、随机种子、最终指标写进一个 csv时间长了回头看能省很多重复试错。这套数据 4000 张的规模单卡训 50 个 epoch 大概几小时一天能跑好几组对照效率足够支撑一轮模型选型。从那以后我每次拿到新数据集都强制先跑通可视化脚本再动模型代码这个顺序帮我省下了太多排查标注问题的时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表