简介面向柑橘类果实病害检测任务的数据集汇集橙子、橘子、桔子常见病害与状态图像覆盖blackspot、canker、fresh、grenning共4个类别采用Pascal VOC与YOLO两种标注格式可直接用于目标检测模型训练。压缩包大小约82.45MB内含2000个文件主要包含xml标注文件和txt说明文件其中xml对应VOC标注txt可用于YOLO格式读取与训练另附使用前必读说明。已有508人学习浏览适合农业智能检测、果树病害识别等方向的开发者与研究人员。数据集特别强调标注对象是果实而非叶子使用时需注意类别名称拼写拿到后即可按VOC或YOLO流程拆分训练集与验证集免去自行采集标注的工作量方便快速验证模型在柑橘病害检测上的效果。1. 拿到2814张VOCYOLO双格式数据后先做一次标注体检做果园巡检或者农产品分拣的检测模型最难的一步往往不是调参而是找到一份能对上现场的数据集。我帮某开发者改过一套柑橘类病害检测的模拟项目X模型在公开的叶子上表现尚可一换成橙子、橘子、桔子的果实特写精度立刻掉十个点。原因不复杂这几类果实外形接近病害斑纹又细通用数据集通常只标“柑橘”一个大类细分类别几乎没有。这个包就是专门针对橙子/橘子/桔子病害检测的数据集共2814张标注图、4个类别同时提供VOC和YOLO两种格式。它适合两类人一是想直接喂给YOLO系训练、不想自己写格式转换脚本的人二是刚入门检测、需要一份干净数据练习全套流程的从业者。第一步别急着开训先花几分钟把标注质量摸一遍。2. VOC与YOLO双格式从目录结构到坐标换算一次讲透2.1 两种目录结构对应两套标注哲学解压这个包之后你大概率会看到两个独立的大目录分别是VOC风格和YOLO风格。VOCVisual Object Classes格式是从早期目标检测比赛沿用下来的图片和标注分开存放标注是XML文件里面不但有边界框坐标还记录着文件名、图片尺寸、分类名这些元信息。YOLO格式则是为深度学习训练做了精简每一张图片对应一个同名TXT文本每行只写五个数字类别索引、归一化中心点x、归一化中心点y、归一化宽、归一化高。一句话概括VOC格式方便人读、方便二次编辑YOLO格式方便模型读、直接进训练脚本。我习惯先看目录结构命令一行就够了# 在数据集根目录下执行查看两层目录结构 find . -maxdepth 2 -type d | sort输出里通常能看到类似这样的关键路径./VOC/JPEGImages ./VOC/Annotations ./VOC/ImageSets/Main ./YOLO/images ./YOLO/labels如果你的解压结果里目录名略有出入比如VOC那套没有ImageSets或者YOLO那套把labels和images平铺在根目录下也不用慌。判断依据很简单看.xml文件和.txt文件的位置VOC格式里XML成堆出现YOLO格式里TXT按图片同名排列。拿到路径后我建议先数一数图片格式后缀因为果实检测里JPEG和PNG混用会直接坑到后面的数据加载脚本。2.2 YOLO标签坐标从归一化反推像素框YOLO的标签文件看起来全是小数第一次接触的人容易心里没底。其实它背后是一条非常直白的换算关系标签里的cx、cy是边界框中心点相对于图片宽高的比例w、h是边界框宽度和高度相对于图片宽高的比例。全部归一化到0到1之间所以换分辨率不需要重新标注。我写了一个很短的脚本把YOLO标签读出来并转换回像素坐标方便你确认框的位置是否合理# load_yolo_label.py # 读取单张YOLO标签并转成像素坐标框 def load_yolo_label(txt_path, img_w, img_h): boxes [] with open(txt_path, r, encodingutf-8) as f: for line in f.read().strip().splitlines(): parts line.split() if len(parts) 5: continue cls int(parts[0]) cx float(parts[1]) * img_w cy float(parts[2]) * img_h w float(parts[3]) * img_w h float(parts[4]) * img_h x1 cx - w / 2.0 y1 cy - h / 2.0 x2 cx w / 2.0 y2 cy h / 2.0 boxes.append((cls, x1, y1, x2, y2)) return boxes if __name__ __main__: # 以一张尺寸为 640x480 的图片为例 boxes load_yolo_label(labels/sample.txt, 640, 480) for b in boxes: print(class:, b[0], pixel_bbox:, b[1:])这段脚本的核心在于把归一化坐标乘回图片宽高再做中心点加减宽高一半的运算最后得到左上角和右下角的像素坐标。你不需要背公式理解“归一化是为了不同分辨率共用同一份标注”就够了。正因为这个特性YOLO标签在数据增强时会比较麻烦随机缩放、旋转后边界框必须跟着做一次几何变换否则标签就废了。所以前几分钟的检查重点看边界框是否超出图片边缘、宽度高度是否为0、类别索引是否超出4这个范围这几类问题在下载数据里偶尔会出现一旦出现就是整批训练信号被污染。2.3 双格式的价值不只是一个文件给你还是两条后路很多人以为同时给VOC和YOLO格式只是为了省去自己转换的麻烦其实更大的价值在于后期处理。VOC的XML里存了图片尺寸你在训练完想统计每个类别的真实尺寸分布、长宽比分布直接用XML解析最方便。YOLO的TXT则适合直接喂给训练脚本改个类别数就能开跑。当你想做切片推理或者做随机裁切增强时往往需要回到VOC格式重新生成标签因为XML里保留了完整的对象级信息TXT的反推虽然可行但遇到多对象框重叠时容易出现边界吞并。我一般这样处理训练阶段用YOLO格式分析和二次加工用VOC格式。两份数据源都放在同一个包里意味着你不需要刻意备份原始标注也不用担心从网上重新找VOC转YOLO脚本时踩到版本坑。需要注意的是包里的VOC和YOLO分类顺序大概率是一致的但不保证第一次索引对齐后以后每次都一致所以进入训练前的类别顺序校验这一步不能省。3. 训练前的三步准备按类别划分数据、可视化检查、分布统计3.1 按类别比例做划分而不是随机洗牌2814张图看起来不少但拆成4类之后每类也就几百张划分不当会让验证集失去代表性。比如纯随机分配某一类占比小分到验证集的个数可能少于10张评估时方差极大昨天还在调参今天就过拟合。更稳的做法是按类别分组再在每个类别内部按比例抽取训练集和验证集。# split_by_class.py # 按类别分组后按 80/20 比例划分训练集与验证集 import os import random from collections import defaultdict random.seed(42) images_dir YOLO/images labels_dir YOLO/labels # 第一步把所有图片按标签中的第一个类别分组 group defaultdict(list) for img_name in os.listdir(images_dir): if not img_name.lower().endswith((.jpg, .jpeg, .png)): continue label_path os.path.join(labels_dir, os.path.splitext(img_name)[0] .txt) if not os.path.exists(label_path): continue with open(label_path, r, encodingutf-8) as f: first_line f.readline().strip() if not first_line: continue cls int(first_line.split()[0]) group[cls].append(img_name) # 第二步每个类别内各自洗牌再按 80/20 切分 train_list, val_list [], [] for cls, imgs in group.items(): random.shuffle(imgs) split_idx int(len(imgs) * 0.8) train_list.extend(imgs[:split_idx]) val_list.extend(imgs[split_idx:]) # 第三步输出YOLO训练脚本可以直接引用的数据清单 with open(train.txt, w) as f: f.write(\n.join(os.path.join(images_dir, name) for name in train_list)) with open(val.txt, w) as f: f.write(\n.join(os.path.join(images_dir, name) for name in val_list)) print(ftrain: {len(train_list)}, val: {len(val_list)})这一段脚本有两个关键点。第一是random.seed(42)固定随机种子否则你每次跑出来的划分结果都不一样模型效果对比就成了玄学。第二是先按类别分组再洗牌而不是全局洗牌这就保证了每个类别都按80比20进入两个集合不会出现某一类全部落在训练集里的极端情况。实际跑完后打印出的数量对比正常情况下大约在训练集2200张出头、验证集550张上下。如果某个类别的样本数不足50后面训练时就要考虑单独给这个类别加权或者做复制增强。3.2 抽10张图画框肉眼确认标注落到病灶上划分完数据接着做标注可视化。这一步不花多长时间但能发现一半以上的数据问题。我用OpenCV把YOLO标签画到原图上随机抽几张看效果# visualize_labels.py # 随机抽取样本把YOLO边界框画到图片上 import os import cv2 import random from load_yolo_label import load_yolo_label images_dir YOLO/images labels_dir YOLO/labels output_dir visual_checks os.makedirs(output_dir, exist_okTrue) sample_images random.sample( [f for f in os.listdir(images_dir) if f.lower().endswith(.jpg)], 10 ) colors [(0, 255, 0), (0, 165, 255), (255, 0, 0), (128, 0, 255)] for img_name in sample_images: img_path os.path.join(images_dir, img_name) img cv2.imread(img_path) h, w img.shape[:2] label_path os.path.join(labels_dir, os.path.splitext(img_name)[0] .txt) boxes load_yolo_label(label_path, w, h) for cls, x1, y1, x2, y2 in boxes: cv2.rectangle(img, (int(x1), int(y1)), (int(x2), int(y2)), colors[cls % 4], 2) cv2.putText(img, str(cls), (int(x1), max(0, int(y1) - 5)), cv2.FONT_HERSHEY_SIMPLEX, 0.8, colors[cls % 4], 2) out_path os.path.join(output_dir, img_name) cv2.imwrite(out_path, img) print(saved:, out_path)这里我复用了上一小节写的标签读取函数所以看起来比较简洁。画框后你要重点看三类现象框是否框住了完整的病害区域比如溃疡病斑周边有没有留出太多背景同一张图里多个框是否大量重叠可能是标注员把一块病斑拆成了几个小框也可能是数据本身是密集病斑框的边缘是否被图片边界截断。如果前十张里有三张以上存在这类问题后面训练出来的模型会继承同样的边界偏差。3.3 一眼看清四个类别分布提前决定要不要特殊处理同类别的图片数量相差三倍以上在果实病害里不算罕见比如炭疽病比疮痂病好标数据量就容易多。我习惯训练前先跑一段统计脚本输出每个类别在训练集和验证集中的数量然后把数量打印出来# 按 train.txt 统计每个类别样本量 python -c from collections import Counter import os c Counter() for line in open(train.txt): txt line.strip().replace(images, labels).replace(.jpg, .txt) if os.path.exists(txt): with open(txt) as f: c[int(f.readline().split()[0])] 1 print(c) 如果四个类别的数字很均衡那就直接进入常规训练。如果某个类别只有别人的三分之一我会先考虑在数据增强里给这个类别更高的复制概率而不是急着设计复杂的采样器。具体操作是在YOLO训练脚本中打开mosaic和随机仿射增加该类别在训练轮次里的出现频率如果增强后验证集还是上不去再考虑专门收集该类别补充数据。一份数据集的复用价值很大程度取决于这类统计做得到不到位。顺便说一句如果训练脚本要求提供data.yaml里面的nc: 4和类别名字列表务必和这里统计到的索引对齐我见过最多的问题就是类别顺序错位导致训练和评估完全不在同一套语义上。4. 避坑果实病害数据集最常见的五个翻车点4.1 训练损失降了但mAP极低验证集全是假阳性现象训练损失曲线一路走低验证集mAP却只有0.1上下把阈值调低后每一张测试图都框出十几个区域。原因这份数据集中大多数图片的背景区域其实很干净但标注策略默认只标病害框没有任何背景负样本。模型在训练时发现“只要多画框就能覆盖所有标注”自然倾向于在验证时到处输出低置信度框。解决先降低conf阈值到0.05观察验证集每张图平均框数。如果平均框数超过5个说明background missing。常见做法是额外收集没有病害的果实图片加入训练集或者把置信度阈值从默认的0.25改成0.5以上强行压掉低分输出。但后一种只是掩盖现象不推荐单独使用。4.2 训练和验证都正常一换到手机实拍就掉点现象用数据集自带的验证集测mAP能达到0.8但部署到现场拍回来的照片上病斑要么漏检要么框偏了半个果实。原因数据集的拍摄背景比较单一果实大小和角度相对固定。训练时网络学到了对背景纹理的依赖一旦现场光照、摆放角度、相机色彩有差异特征分布就偏了。解决训练阶段打开多尺度训练输入分辨率不要固定在一个值而是在640到1280之间随机变化同时把增强里的随机旋转范围从默认值扩大到30度以上因为果实放置角度在真实场景里是任意旋转的。这一步做完实拍掉点的问题通常能缓解一半左右。4.3 一个果子上多个病斑标注却只框了最大的一处现象评估时预测框明明落在病斑上但训练集标注里没有对应框于是被判为false positivemAP被压低。原因这属于标注策略不一致。有的标注员习惯一个对象只框最严重的病灶有人习惯把所有可见斑点都框出来。模型内部学到的是“最多只标一个框”的隐性规则自然和你在推理时对“所有病害都检出”的预期相矛盾。解决检查单张图片的平均框数如果绝大多数图片都只有1个框说明采用的是单实例标注策略。部署时把NMS的IoU阈值从0.45调高到0.6可以减少同一病斑被切成多个框后又被抑制掉的情况。如果你手头时间充足更推荐对这类图片做二次标注把所有可见斑点都补上。4.4 VOC转YOLO时类别顺序对不上训练了两百轮才发现现象训练脚本里data.yaml写的类别顺序和TXT标签索引不一致模型把类别A学到了类别B的位置上。验证集上的AP曲线一直异常画出来的混淆矩阵对角线是斜的。原因VOC的XML里类别名是字符串比如“anthracnose”“canker”转成YOLO时脚本通常会按字母序或第一次出现的顺序生成索引而网络上下载的transformed脚本五花八门有的从0开始有的从1开始一不对齐就全错。解决转换后跑一次索引对齐检查我习惯写一段最简短的校验脚本# 检查类别顺序随机挑一个XML和一个TXT对比类别名与索引对应关系 python -c import glob, xml.etree.ElementTree as ET xml_file sorted(glob.glob(VOC/Annotations/*.xml))[0] txt_file xml_file.replace(Annotations, YOLO/labels).replace(.xml, .txt) root ET.parse(xml_file).getroot() xml_names [obj.findtext(name) for obj in root.iter(object)] txt_names [line.split()[0] for line in open(txt_file) if line.strip()] print(xml:, xml_names, txt_index:, txt_names) 如果XML里的第一个对象是anthracnoseTXT第一行索引是0那这条对应关系就是对的。出现错位时不要手工改TXT里的索引而是重新生成类别顺序文件再统一转换。4.5 果实是圆形边界框是矩形病灶恰好卡在边缘现象病害斑长在果实轮廓边缘矩形框必须把一部分背景包进来导致正样本里混入过多背景特征模型学到的是“圆形区域附近有背景纹理就算病害”。原因YOLO系输出的是轴对齐矩形框无法贴合弯曲的果实边缘这是表示方式的天花板不是模型能力问题。解决优先考虑在增强阶段做随机旋转同时用旋转后的边界框做训练相当于变相扩大了框内的有效特征比例。如果是特别追求边缘贴合的任务可以考虑Mask R-CNN或者将果实分割成多边形后再单独检测但除非场景极度受限一般不建议为了边缘几个像素牺牲YOLO的推理速度。5. 最后一步从平均mAP到单类别诊断再做一份可复现的评估记录训练得到模型权重后多数人会直接看综合mAP高一点就宣布完工低一点就返工调参。但在果实病害这种细分类场景里综合mAP会掩盖单类别的真实水平。我习惯在跑评估时把每个类别的AP单独打印出来然后拿AP最低的类别去反推训练数据的问题。下面这段命令可以在YOLO系框架里输出per-class评估明细# 以常见YOLO训练脚本为例在评估时输出每个类别的AP python train.py --data data.yaml --weights best.pt --val --project runs/eval评估完成后打开results.csv或PR_curve.png重点关注两个指标一是每个类别的PR曲线下面积二是那条类间混淆矩阵。在柑橘类病害检测里溃疡病和疮痂病的初始特征很像最容易出现在混淆矩阵的非对角线位置上说明模型把两类混为一谈。遇见这种情况不要盲目加数据先去原始图片里对比这两类病斑的纹理区别然后回到标注阶段确认边界框是否框得准确因为标注边界模糊会直接放大类别混淆。单类别诊断做完再谈部署。如果这个模型要跑到嵌入式设备或手机端我一般会把输入分辨率从640降到416先试一轮观察AP下降幅度在还能接受的范围内就继续向下压缩到320如果AP掉点超过5个就用剪枝或蒸馏而不是硬降分辨率。训练脚本里的batch size和输入分辨率都要记录到评估表格里我常给表格起名叫eval_notes.csv至少包含这几个字段模型版本、输入分辨率、batch size、每类AP、总mAP、测试时间。有了这份信息后面再调参数时才能判断某个变化是正向还是负向否则只能靠感觉玄学调参。最后说一个我的习惯不管数据集是官方release还是朋友转给我的拿到手先按类别划分、再画框检查、最后统计分布整个过程不会超过二十分钟但这二十分钟能省掉后面好几天的排查时间。2814张图不算大却能支撑一次完整的小目标检测实验循环。希望帮到你。本文还有配套的精品资源点击获取