
简介本资源为苹果缺陷图像语义分割数据集面向从事图像分割算法研究、深度学习模型训练与农业视觉检测方向的开发者及学生可用于训练和评估UNet、SwinUnet、TransUNet等分割网络。数据集已按训练集与测试集划分完毕训练集约3000张图片及对应mask测试集约1200张涵盖健康、病害区域等5类分割标签具体类别可参考classes文件。压缩包共2000个文件以1998个png图像与掩膜为主另含1个txt类别说明和1个py可视化脚本整体约73.28MB。该脚本可随机抽取一张图片展示原始图像、GT图像及GT在原图上的蒙板效果并保存至当前目录便于快速核验标注质量。目前已有180人学习下载适合需要现成数据快速验证分割模型、开展缺陷检测实验的读者。1. 苹果缺陷图像语义分割数据集5 类标注、约 4000 张能直接喂给 U-Net 吗拿到一个「苹果缺陷图像语义分割数据集5 类分割约 4000 张数据和标签」的标题很多人第一反应是把它当成又一个普通图像分割数据集下载、解压、丢进 U-Net 训练脚本然后等着看 mIoU。但真正做过工业质检落地的都知道苹果表面缺陷分割和自然场景语义分割完全是两回事缺陷区域边界模糊、类别间高度相似、光照和果面反光干扰大5 类标签里往往还藏着一类「背景」需要单独处理。这个数据集的价值不在于它有多少张图而在于它把「苹果表面缺陷」这个细分场景的像素级标注做成了可复用的语义分割样本适合做水果分选、产后质检、农业机器人视觉方向的人拿来当基线数据。约 4000 张的规模不算大但足够跑通一个 U-Net 或 DeepLabV3 的完整训练流程也足够暴露你在数据预处理、类别不平衡、标签格式转换上的真实问题。下面按「先搞清楚数据长什么样、再动手转格式和训练、最后讲坑」的顺序展开中间会给出可直接抄的脚本和参数。2. 先看清 5 类标签到底标了什么苹果缺陷语义分割的数据结构拆解2.1 语义分割标签的两种常见存储形态拿到一个语义分割数据集第一件事不是写模型而是确认标签的存储形态。苹果缺陷数据集常见的标签有两种一种是单通道灰度掩膜图mask每个像素值直接对应类别 ID比如 0 背景、1 斑点、2 腐烂、3 疤痕、4 虫眼另一种是彩色掩膜图用不同 RGB 颜色区分类别需要额外一张颜色映射表才能转成类别 ID。前者对训练最友好后者多见于标注工具直接导出的结果。你拿到数据后先用一条命令看标签图的通道数和唯一像素值就能判断属于哪种。# 查看标签目录下第一张图的通道数和像素值分布 python -c import cv2, numpy as np, glob f sorted(glob.glob(labels/*.png))[0] img cv2.imread(f, cv2.IMREAD_UNCHANGED) print(shape:, img.shape, dtype:, img.dtype) print(unique values:, np.unique(img)[:20]) 这段代码用IMREAD_UNCHANGED读取避免 OpenCV 默认把单通道图转成三通道。如果输出 shape 是(H, W)且 unique values 是 0 到 4 的整数说明是标准灰度掩膜可以直接用如果 shape 是(H, W, 3)就需要按颜色映射表转换。参数上唯一要注意的是np.unique只取前 20 个值防止类别多时刷屏。2.2 5 类缺陷的类别定义与样本分布检查5 类分割通常包含背景加 4 类缺陷或者 5 类全是缺陷、背景用 0 表示。不管哪种你都要先统计每类的像素占比因为苹果缺陷分割最典型的问题就是极端类别不平衡——背景可能占 90% 以上虫眼这种小目标可能不到 0.5%。不先做这个统计训练时 loss 会被背景主导模型学出来全是背景mIoU 看着不低但缺陷一个都分不出来。import numpy as np, glob, cv2 from collections import Counter counter Counter() for f in glob.glob(labels/*.png): mask cv2.imread(f, cv2.IMREAD_UNCHANGED) counter.update(mask.flatten().tolist()) total sum(counter.values()) for cls_id in sorted(counter): print(fclass {cls_id}: {counter[cls_id]/total*100:.3f}%)这段统计脚本遍历所有标签图累计每个像素值的出现次数。跑完后你会得到一张类别占比表。如果某一类低于 1%后面就要考虑加权损失或者过采样。这里没有用np.bincount是因为掩膜里可能混入非连续 ID用 Counter 更稳。2.3 图像与标签的命名对齐规则约 4000 张数据里图像和标签必须一一对应但命名规则经常不统一有的数据集图像是apple_001.jpg标签是apple_001.png有的标签多了_mask后缀还有的按文件夹分 train/val但两边文件名对不上。训练前必须写一个对齐检查把没有对应标签的图像和没有对应图像的标签都列出来否则训练时读到空标签会直接报错或者静默跳过你以为是模型不收敛其实是数据没对上。import os img_dir, lbl_dir images, labels imgs {os.path.splitext(f)[0] for f in os.listdir(img_dir)} lbls {os.path.splitext(f)[0] for f in os.listdir(lbl_dir)} print(图像无标签:, imgs - lbls) print(标签无图像:, lbls - imgs)用集合差集找出不匹配项。常见做法是把标签后缀统一去掉再比对如果数据集本身用_mask区分就在生成集合时用f.replace(_mask,)处理。这一步花两分钟能省掉后面几小时的排查。3. 把苹果缺陷数据转成训练可用的格式掩膜、划分与增强3.1 灰度掩膜转类别 ID 与忽略区域处理如果标签是彩色掩膜第一步是转成类别 ID。转换的核心是建立颜色到 ID 的映射字典逐像素查表。这里有个容易翻车的地方标注工具导出的彩色掩膜边缘常有抗锯齿产生的过渡色这些颜色不在映射表里直接查表会变成未定义值。稳妥做法是把未匹配的像素统一归到背景或忽略类通常设 255训练时用ignore_index255跳过。import cv2, numpy as np color_map { (0, 0, 0): 0, # 背景 (255, 0, 0): 1, # 斑点 (0, 255, 0): 2, # 腐烂 (0, 0, 255): 3, # 疤痕 (255, 255, 0): 4, # 虫眼 } def color_to_id(mask_bgr): h, w, _ mask_bgr.shape out np.full((h, w), 255, dtypenp.uint8) for color, cid in color_map.items(): bgr color[::-1] # OpenCV 读入是 BGR match np.all(mask_bgr bgr, axis-1) out[match] cid return outcolor_map的键是 RGB代码里反转成 BGR 再比对因为 OpenCV 默认 BGR。未匹配像素初始化为 255作为忽略类。参数上如果你的数据集背景就是黑色且没有过渡色可以把初始值改成 0省掉忽略逻辑。3.2 按类别分层划分训练集与验证集随机划分在类别不平衡时会让验证集里某些缺陷类几乎消失导致验证指标剧烈波动。正确做法是按图像中出现的缺陷类别做分层抽样保证每个类在训练和验证里都有足够样本。常见做法是先给每张图打一个「包含哪些类」的标签再按这个多标签做分层。import numpy as np, cv2, glob from sklearn.model_selection import train_test_split files sorted(glob.glob(labels/*.png)) labels_per_img [] for f in files: m cv2.imread(f, cv2.IMREAD_UNCHANGED) present set(np.unique(m).tolist()) - {0, 255} labels_per_img.append(sorted(present)) # 用第一个出现的缺陷类做粗分层简单有效 strata [l[0] if l else -1 for l in labels_per_img] train_f, val_f train_test_split(files, test_size0.2, stratifystrata, random_state42) print(len(train_f), len(val_f))这里用每张图里最小的缺陷类 ID 做分层键虽然粗糙但比纯随机稳。test_size0.2对 4000 张来说验证集约 800 张够用。random_state固定住保证可复现。如果你的数据里多类共现很普遍可以改用sklearn的MultiLabelBinarizer加iterstrat但那个需要额外装包上面这个够大多数场景。3.3 针对果面反光的增强策略与参数苹果表面反光强同一缺陷在不同光照下像素差异很大增强不能只用随机翻转。我一般会加亮度对比度扰动、高斯噪声和轻微弹性形变但弹性形变对缺陷边界破坏大参数要保守。下面是一个基于 Albumentations 的增强配置注意ElasticTransform的 alpha 和 sigma 都调小。import albumentations as A train_tf A.Compose([ A.HorizontalFlip(p0.5), A.VerticalFlip(p0.3), A.RandomBrightnessContrast(brightness_limit0.2, contrast_limit0.2, p0.5), A.GaussNoise(var_limit(10.0, 30.0), p0.3), A.ElasticTransform(alpha30, sigma5, p0.2), A.Resize(512, 512), ]) val_tf A.Compose([A.Resize(512, 512)])brightness_limit和contrast_limit控制在 0.2 以内太大模拟不出真实光照反而引入噪声。ElasticTransform的 alpha30、sigma5 是保守值再大缺陷形状就失真了。Resize 到 512 是 U-Net 常用输入显存不够就降到 384。增强只对训练集做验证集只 Resize这点别搞反。4. 用 U-Net 跑通苹果缺陷分割训练脚本与关键参数4.1 数据集类与 DataLoader 的最小实现PyTorch 下写一个语义分割 Dataset核心是读图、读掩膜、同步增强、返回 tensor。掩膜要转成 long 类型因为交叉熵损失要求类别索引是 int64。图像归一化用 ImageNet 均值方差即可苹果图像和自然图像分布接近。import torch, cv2, numpy as np from torch.utils.data import Dataset, DataLoader class AppleDefectDataset(Dataset): def __init__(self, img_files, lbl_files, transformNone): self.img_files img_files self.lbl_files lbl_files self.transform transform def __len__(self): return len(self.img_files) def __getitem__(self, idx): img cv2.imread(self.img_files[idx]) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask cv2.imread(self.lbl_files[idx], cv2.IMREAD_UNCHANGED) if self.transform: aug self.transform(imageimg, maskmask) img, mask aug[image], aug[mask] img torch.from_numpy(img).permute(2, 0, 1).float() / 255.0 img (img - 0.485) / 0.229 # 简化归一化实际按三通道分别做 mask torch.from_numpy(mask).long() return img, maskpermute(2,0,1)把 HWC 转 CHW/255.0归一化到 0-1。这里归一化只写了单通道示例实际要对 RGB 三通道分别减均值除标准差。掩膜用long()转 int64。ignore_index对应的 255 在损失函数里处理Dataset 不用管。4.2 损失函数选择交叉熵、Dice 与类别权重苹果缺陷分割的类别不平衡决定了不能只用普通交叉熵。常见做法是交叉熵加类别权重或者交叉熵和 Dice 损失按比例相加。类别权重可以按像素频率的倒数来设但不要设得太极端否则小类过拟合。我一般用CrossEntropyLoss(weightw, ignore_index255)加一个 0.5 权重的 Dice。import torch.nn as nn class DiceLoss(nn.Module): def __init__(self, ignore_index255): super().__init__() self.ignore_index ignore_index def forward(self, logits, targets): probs torch.softmax(logits, dim1) valid targets ! self.ignore_index targets targets.clone() targets[~valid] 0 dice 0 for c in range(1, logits.shape[1]): # 跳过背景 p probs[:, c][valid] t (targets c)[valid].float() inter (p * t).sum() dice (2 * inter 1e-6) / (p.sum() t.sum() 1e-6) return 1 - dice / (logits.shape[1] - 1)Dice 只对前景类计算背景不参与避免背景主导。1e-6防止除零。类别权重w可以先用[1.0, 5.0, 5.0, 8.0, 10.0]这种经验值再根据验证集表现微调。总损失loss ce_loss 0.5 * dice_loss0.5 这个系数不是固定的小目标多就调到 1.0。4.3 训练循环与验证指标 mIoU 的计算训练循环本身不复杂关键是验证时 mIoU 的计算要排除忽略类并且按类累计混淆矩阵再求平均不能逐 batch 平均。下面给出核心片段。def train_one_epoch(model, loader, optimizer, criterion, device): model.train() for img, mask in loader: img, mask img.to(device), mask.to(device) optimizer.zero_grad() out model(img) loss criterion(out, mask) loss.backward() optimizer.step() torch.no_grad() def evaluate(model, loader, num_classes, device): model.eval() conf np.zeros((num_classes, num_classes), dtypenp.int64) for img, mask in loader: img img.to(device) pred model(img).argmax(1).cpu().numpy() mask mask.numpy() valid mask ! 255 for t, p in zip(mask[valid], pred[valid]): conf[t, p] 1 iou np.diag(conf) / (conf.sum(1) conf.sum(0) - np.diag(conf) 1e-6) return np.nanmean(iou)混淆矩阵按像素累计最后一次性算 IoU。np.nanmean跳过没有样本的类。验证时一定要model.eval()和torch.no_grad()否则显存爆得莫名其妙。学习率用 1e-3 配 Adam或者 1e-2 配 SGDU-Net 上 Adam 收敛更稳。5. 苹果缺陷分割避坑5 个真实踩过的坑5.1 标签像素值不是从 0 连续编号现象训练 loss 一直不降打印标签唯一值发现是 0、1、2、3、5缺了 4。原因标注工具或导出脚本跳号模型输出 5 类但标签最大是 5交叉熵的类别数对不上。解决训练前强制重映射把出现的类别 ID 映射到 0 到 N-1 连续空间并同步更新类别数和权重数组。5.2 图像和掩膜增强不同步导致错位现象训练几个 epoch 后 mIoU 卡在 0.2 上不去可视化发现掩膜和图像旋转角度不一致。原因图像和掩膜分别用了两套增强或者用了只支持图像的增强库。解决统一用 Albumentations 的image, mask同步接口或者自己写增强时对两者用同一组随机参数。这个坑血泪经验查了两天才发现。5.3 验证集里小类样本为零导致 mIoU 虚高现象验证 mIoU 0.75 看着不错但虫眼类一个都分不出来。原因随机划分让验证集里虫眼样本为零nanmean直接跳过该类指标虚高。解决按类别分层划分并在评估时单独打印每类 IoU不要只看平均值。如果某类验证样本少于 10 张指标不可信。5.4 忽略类 255 被当成真实类别参与训练现象模型预测结果里出现 255 这个类别可视化一片白。原因损失函数没设ignore_index255或者 Dataset 里把 255 转成了 long 但没在 loss 里忽略。解决CrossEntropyLoss(ignore_index255)Dice 里也要手动排除 255。检查方法是打印标签最大值如果大于类别数减一就是没处理忽略类。5.5 输入尺寸和显存不匹配导致训练中途 OOM现象前几个 batch 正常跑到一半 CUDA out of memory。原因图像尺寸不统一Resize 没做或者做了但某些图特别大batch 内 padding 到最大尺寸。解决在 Dataset 里统一 Resize 到固定尺寸或者用batch_size1加梯度累积。显存 8G 以下建议 384×384 配 batch 4别硬上 512。6. 把 4000 张用到极致小数据下的迁移学习与难例挖掘技巧约 4000 张对语义分割来说属于小数据从零训练 U-Net 很容易过拟合。我一般会先用 ImageNet 预训练的 ResNet34 或 EfficientNet 做编码器解码器随机初始化学习率编码器设小一点1e-4解码器设大一点1e-3。如果数据集里缺陷形态和自然图像差异大预训练权重可以只用来初始化浅层深层照样从头学。另一个技巧是难例挖掘每个 epoch 结束后把验证集里 IoU 最低的 50 张图挑出来人工检查是标注问题还是模型问题标注错的直接修模型错的下一轮加进训练集重点学。这个循环跑三轮mIoU 通常能涨 5 到 10 个点。验证方法上不要只看 mIoU。苹果缺陷分割最终要落到产线你得看单类召回和误检率。比如腐烂类召回 0.9 但误检 0.3产线上会把好苹果踢掉这个指标比 mIoU 重要得多。我习惯在验证脚本里额外输出每类的 precision、recall 和混淆矩阵混淆矩阵能直接看出哪两类在互相混。常见的是斑点和疤痕混因为颜色接近这时候要么加数据要么在损失里给这两类的混淆对加惩罚项。最后一个具体技巧把 5 类里的背景单独拿出来看。如果背景像素占比超过 95%考虑先做一个二分类的前景分割再在前景区域里做 4 类细分。两阶段比直接 5 类分割在小数据上更稳因为第一阶段任务简单第二阶段只关注缺陷区域类别不平衡的影响小很多。这个思路我在多个工业质检项目里用过比硬调 5 类分割省事。希望帮到你。本文还有配套的精品资源点击获取