苹果缺陷检测数据集VOC+YOLO双格式,YOLO训练避坑指南

发布时间:2026/10/2 9:12:34
苹果缺陷检测数据集VOC+YOLO双格式,YOLO训练避坑指南 简介面向苹果外观缺陷检测的Pascal VOC与YOLO双格式数据集适合需要可直接用于模型训练的标注数据的算法工程师和计算机视觉学习者覆盖病果、好果、烂果、次果四类由labelImg完成标注共17936个标注框。其中烂果样本约14556框病果、次果和好果样本相对较少类别分布信息可帮助使用者在数据增强或样本均衡时做出判断。数据集标注规模为6970张苹果图片每张均有对应的VOC格式XML与YOLO格式TXT标注不含分割路径省去格式转换环节缺陷类别齐全双格式兼顾VOC训练流程和YOLO系列直接加载需求。资源包共2000个文件以XML标注文件为主配TXT说明7z压缩后约292.72MB。已有755人学习下载适合作为苹果分选、品质检测等项目训练的起点数据可直接用于YOLO系列、Faster R-CNN等目标检测模型的训练与效果验证。1. 苹果缺陷检测数据集6970张图、双格式、四类缺陷到底省了多少事苹果缺陷检测数据集这个压缩包我第一次解压后做的第一件事不是开训而是先核对VOC和YOLO两套标注是否对齐。做表面缺陷检测的人都有体会红苹果上的碰伤、果面疤痕和腐烂在RGB图里灰度接近、边界模糊模型最终学成什么样多半不取决于网络结构而是取决于标注干不干净、格式统不统一。这份资源一共6970张图四个类别VOC格式的XML和YOLO格式的TXT都给你备好了等于把“标注格式不统一”这个最大的坑提前填了。适合正在做YOLO系列训练、果实表面缺陷检测、农产品质检算法落地的人。新手可以拿它完整走一遍“解压→校验→划分→训练→调参”的流程老手则可以直接复用它跑网络结构对比实验省掉半天捣鼓标注的时间。对我来说这种双格式数据集最大的价值不是图多而是它天然适合做“格式转换→数据校验→训练验证”的完整演练后面你换任何检测框架都有一份可对照的基准。2. 拆开数据集看门道VOC的XML和YOLO的TXT在苹果表面上各记了什么2.1 一张图三份文件JPEG、XML、TXT的真实对应关系解压之后目录结构通常是下面这种经典组织方式apple_defect_dataset/ ├── images/ # 原图全部JPG │ ├── apple_00001.jpg │ └── apple_06970.jpg ├── voc_annotations/ # VOC格式标注XML文件 │ ├── apple_00001.xml │ └── apple_06970.xml ├── yolo_labels/ # YOLO格式标注TXT文件 │ ├── apple_00001.txt │ └── apple_06970.txt └── classes.txt # 类别清单按id顺序排列每张JPG对应一个XML和一个TXT三个文件的主文件名一致。VOC格式的XML给人复核用的里面包含了图像宽高、目标类别名和绝对像素坐标的bndboxYOLO格式的TXT是给训练器直接吃的每一行是“类别id 归一化中心点x 中心点y 归一化宽 归一化高”五个数字以空格分隔。同一目标在两种格式下的数值差别很大但描述的是同一个物理位置。annotation filenameapple_00001.jpg/filename sizewidth800/widthheight600/height/size object namebruise/name bndbox xmin120/xminymin95/yminxmax180/xmaxymax145/ymax /bndbox /object /annotation对应上面的XMLTXT里那一行是0 0.1875 0.2000 0.0750 0.08330.1875的计算过程是 (120180)/2/800中心点x除以图像宽0.0750是(180-120)/800框宽也除以图像宽。YOLO训练器读取TXT时不会回查原图的像素尺寸它默认你给的五个数都已经归一化到0-1区间。所以VOC转YOLO的脚本本身不难难在类别顺序对齐和边界框取值是否精准这两个点后面章节会专门展开。提示如果你在某个目录里发现TXT文件是空的先别急着删图。空TXT在YOLO里代表这张图没有目标会被当作纯背景训练有时是数据提供方有意混入的负样本。2.2 类别分布与框宽高比四类缺陷在数据里的真实面貌训练前先统计每个类别的框数量这个习惯能救回不少实验事故。拿YOLO格式的TXT做统计最直接因为每行就是一个目标不用解析XML。from collections import Counter from pathlib import Path label_dir Path(yolo_labels) counts Counter() aspect_ratios [] for txt in label_dir.glob(*.txt): for line in txt.read_text().strip().splitlines(): parts line.split() if len(parts) ! 5: print(异常行, txt.name, line) continue cls int(parts[0]) w float(parts[3]) h float(parts[4]) counts[cls] 1 aspect_ratios.append(w / h) for cls in sorted(counts): print(fclass {cls}: {counts[cls]} boxes)这段脚本同时做了两件事按类别累计框数并记录每个框的宽高比。类别id转int是因为TXT第一列在YOLO里恒为整数w/h小于0.3或大于3的框要留意苹果表面缺陷大多是近圆形区域宽高比极端化的框往往标到了果梗、叶片遮挡物或其他噪声。类别分布能直接决定训练策略。四类样本量如果大致均衡直接用默认损失函数就行如果某一类只占5%后面训练时就要考虑给该类加权重或者用复制粘贴增强去补样本。6970张图配上四类缺陷多数情况下分布是够用的但每份数据的实际情况不一样统计完再决定下一步更稳。2.3 配对校验先查缺失再谈训练YOLO训练时会按图片路径去找同名TXT找不到就默认这张图没有标注。最坑的是图片加载成功、训练也不报错但损失函数全程不变最后精度全挂在背景上。from pathlib import Path img_dir Path(images) voc_dir Path(voc_annotations) yolo_dir Path(yolo_labels) all_images list(img_dir.glob(*.jpg)) print(图片总数, len(all_images)) for img in all_images: stem img.stem if not (voc_dir / f{stem}.xml).exists(): print(VOC缺失, stem) if not (yolo_dir / f{stem}.txt).exists(): print(YOLO缺失, stem)跑完这个脚本把缺失文件补上或剔除对应图片再进训练流程。常见做法是直接重命名对齐但更推荐在切分脚本里做一次过滤把不完整的样本干干净净筛掉而不是在原目录里来回搬文件。注意VOC和YOLO两套标注都齐全不代表内容一致。有时间的话随机抽20张图把XML里的bndbox和TXT里的归一化框反算回像素坐标对比一遍确认数据提供方没有在转格式时把某类标错。双格式数据集最常见的隐患就是两套标注里的类别序号对不上。3. 把VOC转成YOLO并喂进YOLOv8转换脚本、7:2:1切分与data.yaml一次配齐3.1 用Python解析VOC的XML到YOLO的TXT核心是归一化虽然这份数据集自带YOLO格式但实际工作中经常遇到只有VOC标注的兄弟项目或者你想把这份标注转成自己团队的中间格式。自己写一次转换脚本比任何教程都更能帮你理解坐标体系。import xml.etree.ElementTree as ET from pathlib import Path # 类别映射务必与目标yaml的names顺序一致 # 实际使用时改成压缩包里classes.txt里的四个类别名 class_map {bruise: 0, rot: 1, scar: 2, stem: 3} voc_dir Path(voc_annotations) out_dir Path(yolo_labels_custom) out_dir.mkdir(exist_okTrue) for xml_file in voc_dir.glob(*.xml): tree ET.parse(xml_file) root tree.getroot() width float(root.findtext(size/width)) height float(root.findtext(size/height)) lines [] for obj in root.findall(object): name obj.findtext(name) if name not in class_map: continue # 跳过未映射类别避免写进脏数据 box obj.find(bndbox) xmin float(box.findtext(xmin)) ymin float(box.findtext(ymin)) xmax float(box.findtext(xmax)) ymax float(box.findtext(ymax)) cx (xmin xmax) / 2 / width cy (ymin ymax) / 2 / height w (xmax - xmin) / width h (ymax - ymin) / height lines.append(f{class_map[name]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) out_path out_dir / (xml_file.stem .txt) out_path.write_text(\n.join(lines))width、height用的float而不是int是为了防止某些标注工具写了带小数点的尺寸时被截断。找到obj.findall(object)遍历每个目标对象再往里取bndbox的四个值。continue那行是防呆设计数据里偶尔出现未纳入分类的杂目标直接跳过比写入错误类别id更安全。输出保留6位小数对800×600级别的图足够。这段脚本跑完可以用上一章的统计脚本再跑一遍对比转换前后的类别框数是否一致。数字对不上多半是XML里存在重复的object节点或空bndbox。3.2 按7:2:1切分并固定随机种子让每一轮实验都可复现训练集、验证集、测试集的划分直接影响你对模型能力的判断。七成训练、两成验证、一成测试是单数据集场景下的默认起手式关键是切分时固定随机种子。import random from pathlib import Path import shutil random.seed(42) imgs sorted(Path(images).glob(*.jpg)) random.shuffle(imgs) n len(imgs) n_train int(n * 0.7) n_val int(n * 0.2) train_imgs imgs[:n_train] val_imgs imgs[n_train:n_train n_val] test_imgs imgs[n_train n_val:] for split, split_imgs in [(train, train_imgs), (val, val_imgs), (test, test_imgs)]: img_out Path(dataset) / images / split label_out Path(dataset) / labels / split img_out.mkdir(parentsTrue, exist_okTrue) label_out.mkdir(parentsTrue, exist_okTrue) for img in split_imgs: shutil.copy(img, img_out / img.name) txt Path(yolo_labels) / (img.stem .txt) if txt.exists(): shutil.copy(txt, label_out / txt.name) print(ftrain{len(train_imgs)} val{len(val_imgs)} test{len(test_imgs)})seed42是约定俗成的固定值保证你和同事分别在各自机器上跑切分结果完全一致。排序后再shuffle也很重要直接从文件系统拿到的顺序往往和采集时间相关不洗牌会让某些类集中到单一split训练集里某类缺陷全是同一光照条件拍的验证时就露馅。test集只在最终评估时用一次平时调参只看val指标。3.3 写data.yaml并跑第一条训练命令YOLOv8和YOLOv5都吃同一套data.yaml结构。路径推荐写绝对路径避免换机器跑训练时相对路径解析错位。# dataset/data.yaml path: /absolute/path/to/dataset # 改成你的实际路径 train: images/train val: images/val test: images/test nc: 4 names: [bruise, rot, scar, stem]names的顺序必须和转换脚本里class_map的value一一对应。这个对应关系错位时训练不报错、loss照常下降但最终mAP会非常难看因为模型学的标签和真实标签是错位的。写完yaml先用一行命令验证标签可读性再正式开训。yolo detect train datadataset/data.yaml modelyolov8n.pt epochs100 imgsz640 batch16 lr00.01 patience20这条命令里epochs100是初始值patience20表示连续20轮验证集指标不涨就提前停imgsz640是YOLOv8默认输入尺寸下面的章节会解释为什么这个值对缺陷检测不能随意调低。第一次跑不要动太多超参先把基线跑出来后续对比实验才有参照物。4. 训练前把好关标注质量三重校验与第一批参数的保守设置4.1 第一重校验归一化坐标越界与类别id溢出标注文件里的坐标越界是最隐蔽的问题。XML和TXT在数据提供方手里可能被手工编辑过或者转换脚本里除整数时精度丢失导致归一化坐标落在0-1之外。import cv2 from pathlib import Path img_dir Path(images) yolo_dir Path(yolo_labels) for txt in yolo_dir.glob(*.txt): img cv2.imread(str(img_dir / (txt.stem .jpg))) if img is None: print(坏图, txt.stem) continue h, w img.shape[:2] for line in txt.read_text().strip().splitlines(): parts line.split() if len(parts) ! 5: print(格式错误, txt.name, line) continue cls, cx, cy, bw, bh map(float, parts) if not (0 float(cx) 1 and 0 float(cy) 1): print(中心点越界, txt.name, line) if cls not in (0, 1, 2, 3): print(类别id溢出, txt.name, cls) if bw 0 or bh 0: print(宽高非法, txt.name, line)cv2.imread读取失败会返回Nonecv2.imread在路径包含中文时会静默失败所以上面先判空。越界坐标在训练时会被Mosaic增强或RandCrop进一步放大一个原本只偏出几个像素的框可能变成边界外的全黑区域损失函数瞬间异常。类别id溢出的危害更直接模型输出维度是4你给它一个5训练直接崩。4.2 第二重校验空标签文件与小目标面积占比空TXT文件在2.3节的配对脚本里不会被发现因为文件存在只是内容为空。YOLO训练器对这类文件的处理是当作背景图少量背景图没问题但如果几十张图全是空的等于人为制造了负样本干扰。from pathlib import Path empty_files [] tiny_boxes 0 total_boxes 0 for txt in Path(yolo_labels).glob(*.txt): lines txt.read_text().strip().splitlines() if not lines: empty_files.append(txt.name) continue for line in lines: parts line.split() if len(parts) ! 5: continue bw, bh float(parts[3]), float(parts[4]) area_ratio bw * bh total_boxes 1 if area_ratio 0.001: tiny_boxes 1 print(空文件数量, len(empty_files)) print(小目标框占比, tiny_boxes / total_boxes if total_boxes else 0)小目标框占比是判断模型选型的硬指标。苹果图多半是整果入镜果面上的碰伤可能只有几十像素换算成归一化面积常常不到0.001。YOLOv8n的检测头对这类小目标召回偏弱如果你统计下来小目标占比超过三成后面直接考虑在输入图层面做切片或者换用更大输入尺寸。4.3 第三重校验完全重叠的重复标注框重复标注在人工标注环节很难避免尤其是多人协作时两个人对同一个缺陷各标一次。完全重叠或高度重叠的框会让模型学出“一个目标必须输出两个框”的错觉推理时置信度被稀释。from pathlib import Path import itertools def iou(a, b): x1, y1, w1, h1 a x2, y2, w2, h2 b ax1, ay1, ax2, ay2 x1, y1, x1 w1, y1 h1 bx1, by1, bx2, by2 x2, y2, x2 w2, y2 h2 ix1, iy1 max(ax1, bx1), max(ay1, by1) ix2, iy2 min(ax2, bx2), min(ay2, by2) inter max(0, ix2 - ix1) * max(0, iy2 - iy1) union w1 * h1 w2 * h2 - inter return inter / union if union 0 else 0 txt_path Path(yolo_labels) / apple_00001.txt boxes [] for line in txt_path.read_text().strip().splitlines(): parts list(map(float, line.split())) boxes.append((parts[1], parts[2], parts[3], parts[4])) for a, b in itertools.combinations(boxes, 2): if iou(a, b) 0.9: print(疑似重复标注, txt_path.name, a, b)这段脚本按文件遍历盒子两两计算IoU超过0.9判定为重复框。如果你的数据集里重复标注比例很高最简单的做法不是删框而是整图剔除因为重叠框周围的上下文信息已经不可靠了。4.4 第一批训练参数为什么是yolov8n、imgsz640、epochs100第一次跑这份苹果缺陷数据集我建议用yolov8n起手不要一上来就上yolov8l。n模型只有约3M参数在单张显卡上十几分钟就能跑完100个epoch它的价值是快速验证数据链路是否通、标注是否有问题。跑通之后再换s或m模型做正式训练即使后面发现数据有坑浪费的也只是十几分钟。imgsz640是YOLOv8的默认值但对苹果缺陷检测来说640意味着把整果压到边长640后再检测。缺陷区域太小的话640不一定够。有V100这类显卡资源时我一般会把imgsz设到800或960试一轮对比mAP变化如果提升不明显再退回640毕竟高分辨率直接拉高显存占用和推理耗时。epochs100配合patience20是稳妥组合。loss曲线如果到80个epoch还在稳定下降说明数据里还有信息可学这时候再手动加epochs续训而不是从头再来。lr0用默认0.01第一次跑不要动它。5. 避坑用苹果缺陷数据集训YOLO最容易翻车的五个位置5.1 训练跑到一半loss全部变nan现象某个epoch开始box_loss和cls_loss同时变成nan训练日志里一片红字。原因最常见是学习率过高加上批次内混入了数值异常图比如纯黑图或压缩损坏图。其次是特征图在深层网络里出现数值溢出YOLOv8的bn层在数据分布极端时方差爆炸。解决先查数据用4.1节的脚本跑一遍剔除坏图和越界标注。再把lr0从0.01降到0.001warmup_epochs从3提到5。如果问题依旧用单卡小batch从零开始训练排除多卡同步带来的数值精度问题。5.2 训练完mAP全是0但loss曲线很好看现象loss正常下降甚至降到很低的平台期但val/epoch的mAP0.5显示0。原因数据集的TXT里类别id和data.yaml的names顺序错位了。比如TXT里的0实际是碰伤yaml里0却是腐烂模型学的内容和验证集对不上所有指标归零。解决打印yaml里的names和TXT里的第一列类别id做对照。最简单的方法是用3.1节的转换脚本重新生成一遍TXT确保class_map的value和yaml顺序完全一致。这个错位在训练时不会报任何警告血泪教训。5.3 验证集召回率低小碰伤框不住现象mAP0.5有0.7以上但小碰伤接近一半漏检小目标框的置信度普遍低于0.3。原因苹果整图在缩放到640后几十像素的缺陷区域只剩个位数像素宽。YOLOv8的每个检测头有固定感受野小目标分配给P3层但特征图的原始分辨率限制了小目标表达能力。解决imgsz从640提到896小目标框数量不降反升。同时把mosaic增强比例调大让训练时更多看到多尺度拼接图。如果硬件条件不允许就做离线切片把原图切成四块分别训练。5.4 混淆矩阵对角线加起来不等于100%现象训练结束看混淆矩阵把每个类别的对角线数值相加发现离100%差很远怀疑自己训练错了。原因YOLOv8的confusion matrix按行归一化并且包含背景类别每一行的含义是“某个真实类别是否预测正确、误判成了哪些类”行和列共用同一个100%基数。拿对角线求和去对准确率本身是错误用法。解决看指标以mAP50、mAP50-95和各类的precision、recall为准。混淆矩阵只用来分析哪两个类别互相误判比如碰伤和疤痕被反复搞混那就回去看标注边界是否符合人工判断习惯。5.5 验证集表现正常部署在产线图上小框误报一堆现象测试集指标合格但把模型部署到实际产线图片同一条苹果上冒出一串小框。原因训练数据里同一缺陷被重复标注或者把果面反光、果粉残留这些背景特征当作正样本标了进去。模型学到了“高亮小区域就是缺陷”的捷径。解决逐张检查重复标注框用4.3节的IoU脚本把所有高度重叠框过滤掉。另外用完全没参与训练的林间实拍图做泛化测试只跑推理不看指标肉眼判断框落点是否合理。误报多大概率是标注里的错误先验不是模型结构问题。6. 上线前做个增强回放用一次“小白鼠”训练验证Mosaic与HSV参数YOLOv8默认开启Mosaic、HSV扰动、随机翻转等增强这些参数在COCO类数据集上调得还行但苹果表面缺陷有自己的特殊性颜色扰动太大会把红苹果的红色学歪Mosaic拼图太碎会把缺陷裁得只剩半个。正式训练前用epochs1跑一个“小白鼠”训练把增强结果回放出来看一眼能避免训练完才发现增强策略不合身。yolo detect train datadataset/data.yaml modelyolov8n.pt epochs1 imgsz640 \ mosaic0.9 fliplr0.5 hsv_h0.015 hsv_s0.5 hsv_v0.4 \ close_mosaic10这一条命令会完整跑一遍增强流程但只训练一个epoch权重没意义有意义的是训练日志里保存的增强中间图。重点看三处Mosaic拼接后缺陷目标有没有被裁出边界HSV扰动后的颜色是否还像苹果翻转方向上缺陷的光影是否出现违背常理的镜像。发现缺陷框大量被裁掉一半就把mosaic降到0.5发现颜色完全失真hsv_h降到0.005发现水平翻转对果面疤痕纹理不友好fliplr设为0.3。我一般会在每个参数上单独试一次增强回放然后拿着增强图让标过数据的同事过目他们说“这图还能认出是苹果”这批参数才敢正式进训练。从那以后我每次拿到VOCYOLO双格式数据集第一件事先用脚本统计类别分布、检查配对缺失再跑一次增强回放确认预处理不毁数据最后才谈训练调参。这套流程看着多花了半小时实际省掉的是三个通宵排障的时间。希望帮到你。本文还有配套的精品资源点击获取