news 2026/9/24 22:03:00

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
苹果缺陷语义分割数据集实战: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.png21_01_03_19_15_00_Canon_top_side_only_msk_6.png。前缀是时间戳年_月_日_时_分_秒,中间是设备标识Canon_top,再往后是拍摄模式all_onside_only,最后msk_6是 mask 相关标识。images 和 masks 里的文件同名,只是所在目录不同,这是语义分割数据集最常见的配对方式。写 DataLoader 时直接按文件名 stem 去 masks 目录找同名文件即可,不需要额外的映射表。

提示:拿到数据后先跑一遍ls train/images | wc -lls 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, split="train", img_size=512): 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(p=0.5), A.RandomBrightnessContrast(p=0.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(image=img, mask=mask) 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", split="train", img_size=512) loader = DataLoader(dataset, batch_size=4, shuffle=True, num_workers=4) model = UNet(in_channels=3, num_classes=5).cuda() # 以 UNet 为例 criterion = nn.CrossEntropyLoss(ignore_index=255) # 255 为忽略像素 optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50) 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_classes=5对应 5 类标签,ignore_index=255是语义分割里的常见约定,标注边界模糊的像素会被标成 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_index=255

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_size=5): 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 大概几小时,一天能跑好几组对照,效率足够支撑一轮模型选型。从那以后我每次拿到新数据集,都强制先跑通可视化脚本再动模型代码,这个顺序帮我省下了太多排查标注问题的时间。希望帮到你。

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

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

手写合规MCP Server:解决Copilot工具调用失败的核心实践

1. 这不是“又一个Node.js教程”,而是一次真实可用的MCP Server手写实践最近在几个开发者群和GitHub讨论区里,反复看到有人问:“Copilot调用不了自定义Tool,是不是Edge 153版本把Copilot干掉了?”、“VS Code里Copilot…

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

WorkBuddy实操指南:从作业批改到错题重练,打造家庭AI助教

家里有个正在上小学的孩子,你就会发现一个残酷的现实:不是每个题家长都讲得明白,更不是每个晚上都有耐心陪着磨作业。作文不会写,数学不会做,英语读完也不知道对不对,这组三连问大概能让一半家长当场破防。…

作者头像 李华
网站建设 2026/9/24 22:02:49

Opus5实现AI原生网站闭环:语义化HTML与多模态协同生成

1. 项目概述:这不是一个“建站”任务,而是一次AI原生内容生产流程的完整闭环验证“《钢铁洪流》官网搞定,纯AI制作,Opus5操刀!”——看到这个标题,我第一反应不是点开链接,而是立刻打开终端、新…

作者头像 李华
网站建设 2026/9/24 22:00:23

Android Activity启动流程全解析:从startActivity到onResume的完整链路

做 Android 开发这几年,我一直觉得能把 Activity 启动过程讲清楚的人,才算真正摸到了 Framework 的门槛。面试的时候,Activity 启动流程几乎是必考题,但大多数人背了一堆时序图,真到排查问题的时候依然一头雾水。我自己…

作者头像 李华
网站建设 2026/9/24 22:00:16

园区能源管理精细化与智能化:现代收费系统的核心价值与落地路径

1. 园区能源管理的现实困境:为什么传统收费模式撑不住了我在园区能源管理这个行当里摸爬滚打了十几年,见过太多园区从建设期的意气风发,走到运营期的焦头烂额。其中最让人头疼的,从来不是设备坏了没人修,而是能源账算不…

作者头像 李华
网站建设 2026/9/24 21:59:59

Pentagi:开源AI Agent驱动的渗透测试辅助系统部署与实践

做安全这一行,时间越久越会发现,真正耗人的往往不是某个“硬骨头”漏洞,而是渗透测试流程里那些重复度极高、又不得不做的环节。端口探测、服务识别、指纹收集、公开漏洞匹配、报告整理,这些工作在每一个项目里几乎都要来一遍。Pe…

作者头像 李华