
简介面向水果与植物叶片病害缺陷检测任务提供一套可直接投入训练的YOLO格式数据集涵盖玉米叶枯病、苹果锈叶、番茄霉叶等30类叶片病害与缺陷类别。图像多为600–2000像素的高分辨率RGB图片每张均包含多个边界框完整的目标标注采用YOLO相对坐标classes, x_centre, y_centre, w, h已按YOLOv5目录结构划分训练集与验证集。压缩包共2000个文件其中1999个txt标签/类别文件与1个Python可视化脚本show.py包体约422.01MBtxt文件无需额外处理即可直接接入训练流程。已有346人学习/下载。可视化脚本支持随机读取图片并绘制边界框便于直观检查标注质量配套的30类别class文本与数据组织方式可显著降低数据清洗和格式转换成本适合目标检测入门实践、叶片病害模型微调及算法对比实验。1. 拿到30类果蔬与叶片病害YOLO数据集先看清它到底给你省了哪三步刚接触YOLO的训练时我踩过最大的坑不是模型崩而是数据整理。现在这份30种水果、植物叶片病害缺陷检测数据集正好把最容易劝退新手的几件事提前做完了YOLO格式标注、train/valid/test划分、类别class文件、数据可视化脚本。你不需要从零标注也不用对着不透明的目录猜类别数量拿过来就能开始训练和调参。它的典型用途是农业视觉、果蔬分选、叶片病害巡检这类场景也适合做毕业设计或小规模瑕疵检测的起步数据。适合两类人一是想快速跑通YOLOv5/YOLOv8流程的初学者二是有自己的数据、想参考这套划分和质检方法做工程化整理的开发者。拿它做迁移学习第一天就能直接进训练环节省下的时间足够你把参数和评估折腾明白。2. 30类class文件不是txt怎么读类名、顺位和id很多人拿到数据集先去看图片这是错的。YOLO数据集的灵魂不在图片里而在labels目录下的那些txt文件里每一行代表一个目标框第一个数字是类别id后面四个数字是归一化后的中心点x、中心点y、宽、高。而这个id对应的是哪个名字完全取决于class文件里那一行的位置。顺序一乱整个模型的输出就全乱了。我还见过一种做法把class文件里的名字按拼音或字母重排觉得“好看”结果所有标签的id全部错位。模型训练完loss很低但预测框对应的类别全是错的这种问题最坑因为它不报错。所以拿到数据集后第一件事不是训练而是把class文件和标签id的对应关系查清楚。2.1 30类class文件怎么排布行号就是模型输出的顺序class文件通常叫classes.txt也有叫labels.txt或class_names.txt的内容长这样apple_healthy apple_black_rot apple_rust apple_scab banana_green banana_ripe banana_rot ...每一行一个类别名行号从0开始算。也就是说第一行的apple_healthy是第0类第二行是第1类以此类推。标签txt里第一列写3就代表这个框属于第3类也就是第四行那个名字。拿到手后我一般先用一段简单的Python读一遍确认类别总数正好是30并且没有空行和重复项。from pathlib import Path classes_path Path(classes.txt) names [ line.strip() for line in classes_path.read_text(encodingutf-8).splitlines() if line.strip() ] print(f类别总数: {len(names)}) for idx, name in enumerate(names): print(idx, name)这段代码用utf-8读取避免Windows下中文乱码strip同时处理掉行尾的换行和可能的回车符。重点看两点第一打印出来的索引是否从0连续排到29第二总数是否恰好是30。如果中间有重复名字后续画混淆矩阵时会发现两个类名重叠很难排查。像这种“水果本身 叶片病害 缺陷”的混合清单常见命名方式是英文小写加下划线比如apple_black_rot、citrus_canker、mango_scab。为什么要强调这个因为中文章节名可以随意写但class文件里最好保持英文和下划线。YOLO在读取names时会把空格当成分隔符如果你在class文件里写“苹果 黑腐病”模型训练可能没问题但可视化脚本和后续部署对接时容易把类名拆成两个词得不偿失。2.2 标签里的id怎么对先看一个标注txt再随机抽三张图class文件确认无误后下一步打开任意一个标签文件看内容。head -5 labels/train/fruit_001.txt正常输出是五行左右每行五个数字例如3 0.5123 0.4478 0.2310 0.1876 0 0.1234 0.5678 0.3456 0.2789第一列数字必须在0到29之间后四个数字是归一化坐标理论上都在0到1附近。如果发现第一列出现30、32甚至100这种数字说明标签和class文件没有对齐后面训练不会报错但模型学出来一定是一团乱麻。光看数字还不够还得看“框画在图上到底对不对”。这里给一个最基础的画框脚本它也是很多数据可视化脚本的核心函数import cv2 from pathlib import Path def draw_label(image_path, label_path, class_names, output_pathNone): img cv2.imread(str(image_path)) if img is None: print(f图片读不到: {image_path}) return h, w img.shape[:2] with open(label_path, r, encodingutf-8) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cls_id int(parts[0]) x_center, y_center, bw, bh map(float, parts[1:5]) x1 int((x_center - bw / 2) * w) y1 int((y_center - bh / 2) * h) x2 int((x_center bw / 2) * w) y2 int((y_center bh / 2) * h) color (0, 255, 0) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) label f{cls_id}:{class_names[cls_id] if cls_id len(class_names) else unknown} cv2.putText(img, label, (x1, max(20, y1 - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) if output_path: cv2.imwrite(str(output_path), img) else: cv2.imshow(check, img) cv2.waitKey(0) class_names [line.strip() for line in open(classes.txt, encodingutf-8) if line.strip()] draw_label(images/train/fruit_001.jpg, labels/train/fruit_001.txt, class_names, check_001.jpg)这段脚本把归一化坐标还原成像素坐标再画到原图上。x1、y1是左上角x2、y2是右下角注意cls_id不能超出class_names长度否则就是标签错位。我习惯在output_path给一个实际路径而不是直接弹窗这样可以在训练前把几十张抽查图保存到一个目录里批量看一眼。这一节做完你才真正“看懂”了一个YOLO数据集。后面所有class文件、data.yaml、模型输出头全都建立在这个顺序之上。3. 把划分好的数据集跑进YOLOv8目录结构、data.yaml与一条训练命令数据可视化脚本和class文件都确认完下一步就是让模型跑起来。这里有个常见误解很多初学者以为把图片和标注放在同一个目录就能训练但实际上YOLOv8要求一个明确的数据集目录结构并且在data.yaml里把路径写清楚。这个数据集既然已经划分好了train/valid/test那目录结构通常长这样你拿到手先核对一下fruit_leaf_defect/ ├── classes.txt ├── data.yaml ├── images/ │ ├── train/ │ ├── valid/ │ └── test/ ├── labels/ │ ├── train/ │ ├── valid/ │ └── test/ └── visualize_boxes.py图片目录和标签目录必须分开放不能混在一起。images/train里的fruit_001.jpg对应labels/train里的fruit_001.txt靠文件名stem匹配。train用于学习valid用于每个epoch结束后评估test在你最终调完参数后做一次完全没见过数据的验证。3.1 目录结构为什么这样分train/valid/test各自管一件事很多开源数据集只有train和valid没有test。这个数据集专门划分了test这一点我很看重。原因很简单valid是训练过程中反复看的模型选哪个epoch没准有“看答案”的成分而test只在最后跑一次结果更接近真实部署。但test目录的标签往往会被忽略。如果你的目标是训练自己的模型并评估泛化能力训练时不要碰test。有些人在训练完后又把test拿去当验证集反复调阈值那这个test就失去了意义。正确的做法是训练时只用train和valid模型训练完毕、阈值选好之后才能用test做最终评估。检查目录是否完整可以用一条命令find fruit_leaf_defect -type f | wc -l find fruit_leaf_defect/labels/train -name *.txt | wc -l第一条输出文件总数第二条输出训练标签数。正常情况下labels/train下txt的数量和images/train下图片数量一致。如果不一致先别训练去查缺失名单后面避坑章节我会专门讲这个。3.2 data.yaml怎么写路径、类别数、类名三件事必须对齐data.yaml是YOLOv8读取数据集的入口也是最容易写错的文件。常见做法是直接用编辑器打开按下面的模板改path: /home/user/fruit_leaf_defect train: images/train val: images/valid test: images/test nc: 30 names: 0: apple_healthy 1: apple_black_rot 2: apple_rust 3: apple_scab 4: banana_green 5: banana_ripe 6: banana_rot注意几个参数的含义。path是数据集根目录的绝对路径train/val/test是相对path的路径不要写成/train这种绝对路径否则在另一台机器上跑就会报错。nc必须等于30和class文件行数一致。names要和class文件顺序完全一致不能这里改了顺序而class文件没改也不能只写了30个名字但顺序错乱。我用过一个土办法来确保data.yaml和classes.txt对齐直接在Python里生成data.yaml而不是手写一劳永逸。import yaml from pathlib import Path names [ line.strip() for line in Path(classes.txt).read_text(encodingutf-8).splitlines() if line.strip() ] data { path: str(Path(fruit_leaf_defect).resolve()), train: images/train, val: images/valid, test: images/test, nc: len(names), names: {i: name for i, name in enumerate(names)}, } with open(data.yaml, w, encodingutf-8) as f: yaml.dump(data, f, allow_unicodeTrue, sort_keysFalse)这段代码把classes.txt的每一行变成names字典的value索引变成key保证data.yaml和class文件天然同步。yaml.dump里sort_keysFalse是为了让names保持0到29的写入顺序不至于按字符串排序变成0、1、10、11这种。注意data.yaml里names字段写成字典和写成列表YOLOv8都认。我习惯写成字典因为如果类名顺序错了一眼就能看到“第5类是banana_ripe”这样的显式对应。3.3 一条命令启动训练从nano模型开始别上来就上l数据准备好了环境装好ultralytics之后训练命令其实就一行yolo detect train datadata.yaml modelyolov8n.pt epochs100 imgsz640 batch16 patience20如果还没下载预训练权重YOLOv8会自动从官方仓库下载yolov8n.pt。这个权重是COCO上训练出来的它和当前30类果蔬病害数据并不完全匹配但作为backbone的初始化参数比随机初始化收敛快得多。这个命令里的关键参数我逐个说一下。data指向data.yamlmodel指定模型结构n是nano版本显存不够或只想快速验证流程时首选。epochs先给100配合patience20意思是连续20个epoch验证集指标不提升就提前停止能省不少时间。imgsz640是训练分辨率如果你的数据集里小目标多可以试试832或1024但显存占用会涨。batch16是经验值如果你只有8G显存nano模型配batch16基本没问题。启动后训练日志会打印出类似这样的信息Ultralytics YOLOv8.2.0 Python-3.10 torch-2.1.0 CUDA:0 Class Images Instances Box(P R mAP50 mAP50-95) all xxx yyy 0.000 0.000 0.000 0.000如果这里显示的类别数量不是30或者显示all下面的行数是空的那一定是data.yaml没读对。训练开始前先停一下用下面这条命令检查data.yaml是否能被正确解析yolo detect train datadata.yaml modelyolov8n.pt epochs1 imgsz640 batch1先跑一个epoch确认日志里出现“30 classes”字样再放开手跑完整训练。这一步能避免你白等几个小时之后发现类别数不对。4. 数据可视化脚本从画框到分布曲线这四张图必跑很多人拿到数据可视化脚本以为只是用来“看个热闹”跑出一张图就完事。但我的经验是可视化脚本最大的价值在于把数据集的隐患暴露在训练之前。类别分布不均、标签坐标越界、小目标过少、图片和标签不配套——这些问题在训练日志里只会表现为loss偏高或mAP上不去但在可视化图里一眼就能看出来。4.1 拿到脚本后先跑什么四张图覆盖四类问题我一般拿到一个划分好的YOLO数据集会先跑四类图第一类是带标注框的样本抽查图比如从train里随机抽30张把框画在原图上检查框的位置是否贴合目标、类别名是否张冠李戴。第二类是类别分布直方图统计每个类别的目标框数量看看长尾是否严重。第三类是框的宽高比分布或面积分布判断有没有大量极小目标。第四类是标签坐标合法性检查找出坐标越界、宽高为0、标签文件里行数格式不对的样本。这四个都用Python脚本做不需要全部写完先跑最紧迫的两个。4.2 类别分布直方图30类水果病害数据最容易出现长尾类别不平衡是这类数据集最普遍的问题。健康样本往往占比很高某种病害样本只有几十个框。直方图脚本如下import matplotlib.pyplot as plt from collections import Counter from pathlib import Path def count_boxes(label_dir): counter Counter() label_dir Path(label_dir) if not label_dir.exists(): return counter for txt in label_dir.glob(*.txt): for line in txt.read_text(encodingutf-8).splitlines(): line line.strip() if not line: continue cls_id int(line.split()[0]) counter[cls_id] 1 return counter class_names [ line.strip() for line in Path(classes.txt).read_text(encodingutf-8).splitlines() if line.strip() ] counts_train count_boxes(labels/train) counts_valid count_boxes(labels/valid) fig, ax plt.subplots(figsize(12, 5)) labels [f{i}:{class_names[i]} for i in range(len(class_names))] train_vals [counts_train.get(i, 0) for i in range(len(class_names))] valid_vals [counts_valid.get(i, 0) for i in range(len(class_names))] ax.bar(labels, train_vals, labeltrain, alpha0.7) ax.bar(labels, valid_vals, labelvalid, alpha0.7) plt.xticks(rotation90, fontsize8) plt.legend() plt.tight_layout() plt.savefig(class_distribution.png, dpi150)这个脚本统计的是“目标框数量”而不是“图片数量”因为一张图里可能同时出现三个苹果三个框才算三个实例。跑出来后重点看两个东西第一有没有类别在train里超过2000个框、在valid里却只有20个框这种极端不匹配说明划分有问题第二有没有类别总数小于50这类样本后期训练很容易被模型忽略。如果某个类别框太少常见的处理方式不是硬删而是做针对性的复制粘贴增强或者直接用mosaic增强让模型多看到它。如果连valid里都凑不出足够的该类样本那这个类的评估指标天然就不稳定只能当作“检测到就算赢”的弱指标。4.3 标签坐标越界检查别等到训练完才发现有脏标签第二个必须跑的是坐标合法性检查。YOLO格式的标签四个数值都是归一化的理论上x_center、y_center、w、h都应该在0到1附近。实际标注时边缘目标很容易标出1.01这种值画框时超出图片边界训练时YOLO虽然会裁剪但框的中心和尺寸信息已经变了。from pathlib import Path def check_labels(image_dir, label_dir, tolerance0.05): problems [] image_dir Path(image_dir) label_dir Path(label_dir) for txt in label_dir.glob(*.txt): img_candidates [ image_dir / (txt.stem ext) for ext in [.jpg, .jpeg, .png, .JPG, .JPEG, .PNG] ] img_exist any(p.exists() for p in img_candidates) if not img_exist: problems.append((txt.name, missing image)) for line in txt.read_text(encodingutf-8).splitlines(): parts line.strip().split() if len(parts) ! 5: problems.append((txt.name, fbad format: {line.strip()})) continue cls_id int(parts[0]) cx, cy, w, h map(float, parts[1:5]) if w 0 or h 0: problems.append((txt.name, zero width or height)) if not (0 cls_id 29): problems.append((txt.name, fclass id out of range: {cls_id})) if cx - w / 2 -tolerance or cx w / 2 1 tolerance: problems.append((txt.name, bbox x out of range)) if cy - h / 2 -tolerance or cy h / 2 1 tolerance: problems.append((txt.name, bbox y out of range)) if not problems: print(标签全部正常) else: print(f发现问题 {len(problems)} 条) for name, reason in problems[:30]: print(name, reason) check_labels(images/train, labels/train)tolerance取0.05意思是允许坐标轻微越界5个百分点。如果越界值超过这个范围大概率是标注软件导出时的坐标换算错误。这类错误通常只出现在几十个文件里花十分钟修掉比训练完成后发现某个类别mAP异常要省事得多。5. 避坑YOLO数据集训练里最容易翻车的5个问题这一章我会把实际训练这类数据集时最容易踩的坑列出来每一条都是我自己翻过车或者帮别人排查过的问题。别小看这些细节它们不会让程序报错但会让模型效果离你的预期越来越远。5.1 图片和标签文件名不匹配一训练就No labels found现象训练日志里显示训练集图片数量正常但标签数量为0训练loss完全不下降或者valid的mAP一直是0。原因最常见的是图片扩展名大小写不一致。比如images目录里图片叫fruit_001.JPG而labels目录里对应的文件叫fruit_001.txt这没问题。但如果你的图片是fruit_001.JPG而另一份拷贝里被人改成了fruit_001.jpg在Linux系统下就是两个不同的文件名YOLO找labels时按stem匹配会直接认为没有标签。还有一种情况是图片文件名和标签文件名前缀不一致比如图片叫apple_20240101_001.jpg标签却叫img_001.txt。解决先做一次文件名配对检查找出所有没有对应标签的图片和没有对应图片的标签。from pathlib import Path image_dir Path(images/train) label_dir Path(labels/train) img_stems {p.stem for p in image_dir.iterdir() if p.suffix.lower() in [.jpg, .jpeg, .png]} lab_stems {p.stem for p in label_dir.glob(*.txt)} only_img img_stems - lab_stems only_lab lab_stems - img_stems print(f有图片没标签: {len(only_img)} 个) for s in sorted(only_img)[:10]: print( , s) print(f有标签没图片: {len(only_lab)} 个) for s in sorted(only_lab)[:10]: print( , s)如果only_img里全是. JPG的图片而标签是小写前缀直接用rename统一成小写扩展名。如果前缀根本不匹配那就得回到标注导出工具里重新生成标签别用改名硬凑。5.2 class文件顺序与标签错位loss很低但预测全错现象模型训练时loss下降很漂亮验证mAP也不低但把图片拖进预测脚本里一看框的位置准类别名全是乱的。比如苹果黑腐病被标成香蕉成熟而且很有规律地整体偏移几位。原因class文件在标注完成后被人为改动过。最常见的是“把类名按字母排序整理了一遍”或者“删掉了某个不用的类别没有重新映射其它id”。标签文件里的第一列是老的类idclass文件顺序变了模型输出的类别名就跟着错位。解决先把class文件恢复成标注时的原始顺序。如果你不知道原始顺序就只能从标签id频率反推或者抽样看每类框的目标形态。这里有个土办法统计每个类别id的框数量数量最多的往往是健康类别再结合图片样本人工核对。修好class文件后必须在可视化脚本里抽样三到五张图确认“第5类画出来的确实是香蕉”再开训练。5.3 BN崩溃训练第二个epoch就出NaN现象loss在前几个iteration正常某个iteration突然变成nan后面再也回不来。重启训练可能复现也可能换一个batch才出现。原因batch size太小加上学习率偏大BatchNorm层的统计量在训练初期不稳定。30类数据集如果某些病害图片光照差异极大前几个batch的输入分布方差特别大BN层很容易被极端值带偏。特别是你如果没加载预训练权重从头训练时bn的running_mean和running_var都是初始状态更敏感。解决先把初始学习率调低比如yolov8默认lr00.01在自定义数据集上建议改成0.001或0.0005。然后把batch加大实在显存不够就换更小的nano模型。最后一定加载预训练权重。这三个办法都做了还崩溃再考虑在data.yaml里加一句话augment: false先用最朴素的输入跑通一个epoch确认数据本身没问题再逐步打开增强。5.4 混淆矩阵总和不一样同一份验证集跑两次结果对不上现象同一份数据集同样权重跑两次混淆矩阵数字总和不一样。有人为此怀疑自己数据处理有bug。原因混淆矩阵统计的是最终预测结果而预测结果受conf阈值和NMS参数影响。YOLO在验证时会做非极大值抑制同一个目标框在不同iteration里可能被保留或抑制尤其是重叠严重的目标。此外如果验证集里有重复或高度相似的图像也会导致两次统计的表观差异。这不算模型bug而是目标检测评估的固有现象。解决固定conf和iou再看矩阵。YOLO命令行验证写法yolo detect val datadata.yaml modelruns/detect/train/weights/best.pt conf0.001 iou0.7 plotsTrueconf0.001是为了让低置信度预测也参与统计这样矩阵能真实反映模型“疑似框”的分布plotsTrue会生成混淆矩阵图。记住你对比的应该是“不同模型在同一阈值下的矩阵”而不是“同一个模型在不同阈值下的数字能不能对得上”。5.5 训练集和验证集数据泄漏指标好看但上线就崩现象训练和验证的mAP都很高模型看起来完美一拿到新场景拍的图片去推理召回率暴跌漏检一堆。原因划分数据集时做了随机划分同一个水果或同一片叶子在train和valid里各出现了一次。因为相邻拍摄角度差异很小模型在验证时等于开卷考试真正到线上遇到没见过的角度就露馅。解决这类病害检测数据集的正确划分方式是按“场景”划分。比如一批照片来自同一棵树的连续拍摄那就让这一整批只落进train或只落进valid不能按单张图随机分。检查方法也简单用图像哈希比较一下train和valid里的相似图片pip install imagehash这个工具能算出每张图的感知哈希把两边的哈希比对一下重复率高于10%基本就是泄漏了。这个数据集的划分如果你发现already done也要自己再跑一遍这个检查因为不同团队对“相似”的判定标准不一样。6. 进阶预训练权重、置信度门限与混淆矩阵的验证用法数据没问题、训练能跑通之后接下来的价值点全在评估和部署调优上。第一个技巧是迁移学习。加载预训练权重后前20个epoch建议冻结backbone只让检测头学习当前30类数据的特征映射。命令行写法是yolo detect train datadata.yaml modelyolov8n.pt freeze10 epochs100 imgsz640 batch16freeze10表示冻结前10层对nano模型来说就是整个backbone。先用冻结方式跑30个epoch让检测头稳定再解冻全模型、把学习率降到0.0001继续训练这是我能给出的最稳的训练流程。第二个技巧是置信度门限的选定。训练完不要直接用默认conf0.25去跑test而是把conf从0.05到0.5扫一遍看precision和recall的拐点然后按下发场景选阈值。做识别率优先的巡检场景就选recall高的低阈值做出货分选这种误检代价高的场景就选precision更高的阈值。这个决策一定要发生在看test结果之前不然你就是在对着答案选阈值。第三个技巧是混淆矩阵的对齐。验证生成的混淆矩阵图里行和列的名字来自data.yaml的names字段如果你发现图上某个类名对不上说明data.yaml里的names顺序和训练时的class文件不一致这是经常被忽略的地方。我习惯把confusion_matrix.png和数据可视化脚本出的class_distribution.png放在同一个目录下看如果某个类别训练样本很少混淆矩阵里那行数字天然就乱别急着调模型先去补数据。最后说一个我自己的习惯每次改动数据集或class文件先用数据可视化脚本全套跑一遍再训练绝不嫌麻烦。翻车翻多了就明白绝大多数训练事故都不是模型问题而是数据在某个环节出了错。希望帮到你。本文还有配套的精品资源点击获取