
简介面向农作物识别与健康检测场景的目标检测数据集包含4800张已标注图片覆盖10种作物的健康样本和27种病害样本其中24种病害附带程度分析3种发病程度相似数据分布均匀、贴合实际项目。图片清晰、场景广泛已按训练集、验证集和测试集划分可直接用于深度学习目标检测模型的训练与效果评估。资源包共2000个文件以1997张jpg原图为主另有2个json标注文件和1个txt说明文件整体约940MB解压即可投入工程化使用省去收集、筛选和标注的时间成本。该数据集适合农作物病害监测系统的算法研发也可用于高校科研与教学中的图像分类、目标检测实验。目前已有542人学习下载对计算机视觉与智慧农业方向的开发者具有直接参考价值。1. 农作物病害数据集先别急着训练先把它当成一张体检报告农作物病害数据集这类资源常被当成“拿来就训”的图片压缩包解压、跑个分类、看准确率。但真到了农作物识别和健康检测场景你会很快发现模型好不好用不取决于训练轮数而取决于你拿到数据集后做的第一轮体检。这个标题下的数据集通常覆盖多种作物、多种病害类型和健康样本能用于图像分类、目标检测、异常筛查等方向。这篇文章会把从目录整理、标签清洗到分类与检测落地的完整路径讲清楚并指出那些让验证集虚高、上线翻车的隐藏问题。适合正打算用公开作物病害数据集做识别但不想在数据阶段走回头路的人。2. 这类数据集到底是什么目录结构、标签粒度与选型判断2.1 两种最常见的组织格式分类文件夹和检测标注框我经手过的农作物病害数据集九成是两种形态。第一种是 ImageNet 风格的文件夹结构训练集、验证集、测试集下面按类别建子目录类别名一般是三段式例如tomato__leaf_mold__healthy、corn__gray_leaf_spot__healthy这种格式天然适合做图像分类用torchvision.datasets.ImageFolder可以直接读不需要自己写 Dataset 类。第二种是带目标检测标注的图片目录之外还有一份 VOC XML 或 YOLO txt 标注除了类别名还给了每个病斑或叶片的外接矩形框。这种格式适合回答“病在哪里”的问题比如你要做一个田间巡检机器人光知道“这株番茄有叶霉病”不够还得让机械臂对着病斑喷药。判断你手上是哪一种不要只看文件后缀。有些数据集表面给了labels目录点进去是空的或者只有一份classes.txt实际上并没有框。正确做法是统计标注文件数量跟图片数量对比一下。正常情况下一张叶子图可能有两个框一个叶斑、一个背景虚化区域如果标注文件数量明显少于图片说明这份数据集的检测标注是稀疏的或者是只有一部分图被标过这种数据直接拿来训练 YOLO 很容易让模型学会“看见叶子就画框”。另外提醒一句现在热词里经常出现plantdoc数据集下载PlantDoc 是一份公开的作物病害检测数据集标注风格和很多自采数据集不一样它的框更多框在整片叶子上而不是单个病斑。如果你的场景是“病斑定位”不能拿 PlantDoc 的框直接替换要重新看框的语义。2.2 标签粒度和作物覆盖先确认你的场景是“识别”还是“健康检测”这个标题里同时写了“农作物识别”和“健康检测”这两个词指向的标签粒度完全不同。农作物识别解决的是“这是什么作物”水稻、小麦、玉米、番茄、辣椒类别少则三五类多则十几类类间差异大模型很容易学。健康检测解决的是“这株作物有没有病、什么病”病害类别通常按病理名字走比如稻瘟病、小麦锈病、玉米大斑病、番茄晚疫病。同一个病害在不同发病阶段叶片外观差异极大早期可能只是几个褪绿小点后期整片叶子枯黄卷曲。这就是为什么很多团队拿到公开的农作物病害数据集后第一个分歧点就发生在标签粒度上。我一般会建议先做一次“场景降维”如果你的现场应用只需要区分健康和发病就把所有病害类别合并成一个disease类和healthy做二分类。这样做的好处是类别平衡问题大幅缓解因为多数公开数据集中健康叶片数量远多于单个病害类别。如果你的应用需要给出具体病害名那就保持原始细粒度标签但必须接受一个事实某些类别的样本可能只有几十张此时小样本类别的增广策略比模型结构更重要。还有一类“健康检测”指的是营养元素缺素、干旱胁迫这类非病原性异常比如缺氮叶片发黄、缺钾叶缘焦枯。绝大多数公开农作物病害数据集不覆盖这些标签因为它们要靠土样和叶面化学成分才能确诊只看图片容易误标。所以拿到数据集后先看classes.txt或目录名里有没有缺素、旱害这类词如果有说明这份数据集的“健康检测”范围更广如果没有你的模型只能检测病理病害缺素场景必须另找数据不能硬训。2.3 拿到手先做三件事统计类别、检查图片、看标注一致性第一件事不是写训练脚本而是写一个目录统计脚本把每个类别的样本数打出来。我见过太多人跳过了这一步训到一半发现某个类只有 20 张图验证集准确率被这个类拖到 0.2 才发现问题。下面这段脚本按 train/val/test 三个根目录分别做统计import os from collections import Counter roots [train, val, test] for root in roots: counter Counter() for dirpath, dirnames, filenames in os.walk(root): if len(filenames) 0: continue # 相对于当前根目录的路径就是类别名 cls os.path.relpath(dirpath, root) counter[cls] len(filenames) print(root, dict(counter))这段代码的逻辑是按目录名做聚合统计每个类别下的图片数量。需要注意脚本里if len(filenames) 0的过滤条件它能把空目录直接跳过否则relpath会把空目录也变成一个“类”。如果统计结果显示小于 50 张的类超过三个我建议先做同类合并或者为这些类单独设计过采样策略。第二件事是检查图片是否损坏。公开数据集在网盘转存、压缩、上传下载过程中经常出现截断的 JPEG 文件读图时能打开但load()会抛异常。这类坏图放进训练集会让模型在推理时对同类图片产生莫名其妙的抖动。用PIL逐张验证是个稳妥做法from PIL import Image import os broken [] for root, dirs, files in os.walk(raw_dataset): for f in files: if not f.lower().endswith((.jpg, .jpeg, .png)): continue path os.path.join(root, f) try: img Image.open(path) img.load() # 不调用 load 很多解码错误不会暴露 except Exception: broken.append(path) print(broken files:, broken)这里的关键是img.load()只Image.open()是惰性解码文件头损坏但不影响打开。坏图不要直接删先看路径集中在哪个类别如果集中在某一类可能是该类原始采集就有问题删除后要重新评估类别数量。第三件事是看标注框坐标是否越界尤其是从公开来源转存的数据集有些 XML 里xmin/xmax比图像宽还大训练 YOLO 时会被归一化到大于 1 的坐标损失直接变成 NaN。这一步没有捷径建议写脚本把每张图的标注框和图像宽高比对一遍越界的框要么裁掉要么修正。3. 用它训练一个农作物分类模型从划分数据集到微调 ResNet3.1 按作物-病害-健康结构划分 train/val/test很多公开数据集下载下来就自带划分但我不建议直接信任它。常见问题是验证集和训练集来自同一个采集批次光照、土壤背景高度相似验证集准确率虚高到了自己的田间数据就掉点。最稳妥的做法是自己重新划分而且必须按“类别”做分层抽样不能做全局随机划分否则某个类别的全部样本可能都进了训练集验证集里直接缺类。我自己常用的划分脚本要求原始目录是Dataset/Class/xxx.jpg这种三层结构下面这段代码按类别分别 shuffle 后切分保证每个类别在三个集合里都出现import os import shutil import random src_root raw_dataset splits {train: 0.8, val: 0.1, test: 0.1} seed 42 random.seed(seed) for cls in os.listdir(src_root): cls_path os.path.join(src_root, cls) if not os.path.isdir(cls_path): continue files [f for f in os.listdir(cls_path) if f.lower().endswith((.jpg, .jpeg, .png))] random.shuffle(files) n_val int(len(files) * splits[val]) n_test int(len(files) * splits[test]) n_train len(files) - n_val - n_test # 切分顺序固定为 train - val - test保证互不重叠 for split_name, split_files in { train: files[:n_train], val: files[n_train:n_train n_val], test: files[n_train n_val:], }.items(): out_dir os.path.join(split_name, cls) os.makedirs(out_dir, exist_okTrue) for f in split_files: shutil.copy2(os.path.join(cls_path, f), os.path.join(out_dir, f))这里用的shutil.copy2而不是shutil.move因为原始数据集是一次性资源移动过去之后如果划分比例需要调整还得重新下载。按类别划分的意义在于病害类别数量差异大如果全局随机切分有些小类在验证集里可能一张都分不到。n_val和n_test用int()取整当类别样本数少于 10 时验证集可能只有 1 张这会影响评估稳定性此时需要对该类做增广后再切分。3.2 用预训练权重做迁移学习的小型训练脚本训练农作物病害分类我不建议从头训练一个 CNN。这类数据集类别多但单类样本少从头训练容易过拟合。常见做法是用 ImageNet-1k 预训练权重做迁移学习把最后一层全连接换成自己数据集的类别数。如果你以前下载过imagenet1k数据集下载的压缩包可能知道它只适合做预训练不能直接当训练集而torchvision里自带的权重文件足够我们做微调。下面这个脚本基于 PyTorch模型用 ResNet50只训练 15 个 epoch。病害图像类别之间的差异集中在叶片的纹理和颜色分布上不需要特别深的网络ResNet50 在精度和训练速度之间比较平衡import torch import torchvision.transforms as T from torchvision.datasets import ImageFolder from torch.utils.data import DataLoader from torchvision.models import resnet50, ResNet50_Weights transform_train T.Compose([ T.RandomResizedCrop(224, scale(0.6, 1.0)), T.RandomHorizontalFlip(), T.RandomRotation(15), T.ColorJitter(brightness0.2, contrast0.2, saturation0.2), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) transform_test T.Compose([ T.Resize(256), T.CenterCrop(224), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) train_ds ImageFolder(train, transformtransform_train) val_ds ImageFolder(val, transformtransform_test) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_ds, batch_size64, shuffleFalse, num_workers4) model resnet50(weightsResNet50_Weights.IMAGENET1K_V1) model.fc torch.nn.Linear(model.fc.in_features, len(train_ds.classes)) optimizer torch.optim.SGD(model.parameters(), lr1e-3, momentum0.9, weight_decay1e-4) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size5, gamma0.1) criterion torch.nn.CrossEntropyLoss() for epoch in range(15): model.train() for images, labels in train_loader: optimizer.zero_grad() preds model(images) loss criterion(preds, labels) loss.backward() optimizer.step() model.eval() correct total 0 with torch.no_grad(): for images, labels in val_loader: preds model(images) _, predicted preds.max(1) total labels.size(0) correct (predicted labels).sum().item() print(fepoch {epoch 1}: val_acc {correct / total:.4f}) scheduler.step()这里用ResNet50_Weights.IMAGENET1K_V1而不是随机初始化是迁移学习的关键。病害图片经过RandomResizedCrop和ColorJitter之后模型看到的仍然和 ImageNet 自然图像有较高的低层特征重合比如边缘、纹理、颜色渐变这些低层特征在预训练时已经学好了我们只需要微调高层语义特征。优化器选择 SGD 而不是 Adam是因为这个任务类别差异小SGD 配合动量在小 batch 下更容易收敛到平坦极值Adam 前期冲得快但后期容易在验证集上震荡。step_size5表示每 5 个 epoch 学习率乘以 0.1病害数据集的训练集不大一般不需要更复杂的学习率曲线。3.3 训练参数怎么定学习率、batch size、图像尺寸背后的权衡迁移学习的初始学习率不要直接照搬分类网络论文里的 0.1那是对从头训练来说的。微调 ResNet50 时1e-3 起步是安全的如果数据集类别数超过 20或者单类样本量很小建议降到 3e-4。怎么判断学习率是否过大看第一个 epoch 的 loss 变化如果 loss 在前几十个 batch 里不降反升并且验证集准确率低于随机猜测说明学习率太大直接减半。batch size 的选择受显存限制但我建议不要小于 16。农作物病害图里病斑通常只占叶片的一小块区域batch 太小会让 BatchNorm 的均值和方差估计不稳尤其 ResNet 前面的层还在微调时BatchNorm 的统计量需要足够多的样本才稳定。如果你只能跑 batch size 8那就要考虑用torchvision.ops里的BatchNorm冻结选项或者干脆用更大的模型但降低分辨率。图像尺寸上公开数据集里的图片很多是 600×600 以上甚至有 1920×1080 的。直接缩到 224×224 会丢失病斑细节但增大到 384×384 会让训练时间接近翻倍。我的经验是如果病斑直径在叶片面积的 10% 以下224 输入大概率学不到细节至少用 320 输入。配合RandomResizedCrop(320, scale(0.5, 1.0))模型能看到更完整的病斑纹理。要注意的是改输入尺寸时预训练权重的平均池化层会自动适配不需要额外改网络结构。4. 从“是什么病”到“病在哪”转成 YOLO 格式做目标检测4.1 把分类数据集改造成检测数据集的常见做法题目里写了“适用于农作物识别、健康检测等场景”实际部署中光有分类结果不够。比如无人机巡田时模型说“这块田有玉米大斑病”但地面植保人员还是不知道病斑集中在哪几株。所以很多团队在分类模型跑通后第二步就是把数据集改造成检测格式。如果原始数据集自带检测框这一章可以跳过如果只有分类标签常见做法是先用分类模型对每张图做 Grad-CAM 或类似注意力可视化把响应区域的外接矩形当成初始伪框再用 LabelImg 或 X-AnyLabeling 人工修正一轮。这个过程听起来繁琐但对一个 5000 张的中等规模数据集两人一天能完成初标比从零框选快得多。注意伪框不能直接当最终标注因为 Grad-CAM 的响应区域经常覆盖整片健康叶脉和真实病斑边界差很远只能作为标注起点。还有一条更省事的路直接用公开检测数据集里的数据来扩充。前面提到的 PlantDoc 就是带框的作物病害检测数据如果它包含的作物种类和你的场景有重叠把它的标注合并进来能明显提升模型对叶片边界的定位能力。合并且标注框语义一致时类别 id 映射要重新对齐不能直接拼接两个数据集的classes.txt。4.2 标注框归一化与类别 id 映射脚本无论你拿到的是 VOC XML 还是 LabelMe JSON训练 YOLO 前都要转成 YOLO 格式的 txt 文件。YOLO 格式每行一个目标类别 id、归一化后的中心点 x、中心点 y、宽度 w、高度 h五个值都是 0 到 1 的浮点数。写转换脚本前先确认标注框坐标是相对于原图分辨率还是已经缩放过的我遇到过数据集里图片已经 resize 但标注没跟着 resize这种数据转换后框全偏了。下面这段脚本处理 VOC XML假设图片宽高能从 XML 的size节点读取import xml.etree.ElementTree as ET def convert_voc_to_yolo(xml_path, class_names, img_w, img_h): tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.iter(object): name obj.find(name).text if name not in class_names: continue cls_id class_names.index(name) bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # 有些标注工具是闭区间宽高需要加 1 再归一化 box_w max(xmax - xmin, 1.0) box_h max(ymax - ymin, 1.0) cx (xmin xmax) / 2.0 / img_w cy (ymin ymax) / 2.0 / img_h w box_w / img_w h box_h / img_h # 坐标越界直接跳过别让 YOLO 在 NaN loss 里挣扎 if cx 0 or cy 0 or w 0 or h 0 or cx 1 or cy 1: print(fskip bad box in {xml_path}: {cx}, {cy}, {w}, {h}) continue lines.append(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) return lines这里有两个容易踩的细节。一是box_w xmax - xmin如果标注工具把像素坐标当成闭区间真实宽度应该是xmax - xmin 1但对几百像素的框来说差 1 像素影响不大所以脚本里加了max(..., 1.0)防止宽度为 0。二是越界框的处理策略我选择跳过而不是截断到 0 到 1因为越界框往往不只是坐标问题背后可能是图像和标注错位截断后模型会学到错误边界。转换完要检查每张图片是否存在同名 txt且 txt 非空如果一张图没有任何标注在 YOLO 训练里会被当成背景图过多背景图会让模型倾向于不画框。4.3 用 YOLOv8 在自己的病害数据集上跑通训练现在说到yolov8训练自己的数据集这个场景它已经是目标检测落地最顺的一条路。数据集目录按下面的结构放images 和 labels 各自分成 train/val 两个子目录图片和 txt 文件名必须一一对应agriculture/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yamldata.yaml 是关键配置路径最好写绝对路径写相对路径时 yolo 命令的工作目录不同会导致找不到数据集。nc是类别数names顺序必须和转换脚本里class_names的顺序完全一致否则类别就乱了path: /absolute/path/to/agriculture train: images/train val: images/val nc: 5 names: [tomato_healthy, tomato_leaf_mold, tomato_late_blight, corn_healthy, corn_gray_leaf_spot]训练命令用 ultralytics 的命令行接口就行它会自动读取 data.yaml 里的路径# 用 COCO 预训练权重 yolov8n.pt 做迁移学习 yolo train modelyolov8n.pt dataagriculture/data.yaml epochs100 imgsz640 batch16 device0 # 验证最佳权重结果会打印每个类别的 mAP50 和 mAP50-95 yolo val modelruns/detect/train/weights/best.pt dataagriculture/data.yaml # 导出 ONNX方便后面做 TensorRT 或 OpenVINO 部署 yolo export modelruns/detect/train/weights/best.pt formatonnx imgsz640yolov8n.pt是 nano 版本适合嵌入式设备但如果你的作物种类多、病斑小建议换成yolov8s.pt或yolov8m.pt。imgsz640是训练时输入尺寸和分类不同检测任务对病斑小目标很敏感如果病斑在原图中占比很小可以把 imgsz 提到 960但显存占用会显著上升。训练时注意观察 loss 曲线YOLOv8 的 box loss 在前 20 个 epoch 下降很快后 80 个 epoch 是缓慢精调如果 box loss 从一开始就在震荡优先排查标注框有没有错位而不是调学习率。5. 避坑专题农作物病害数据集上的五个真实踩坑记录5.1 同一病害在不同叶龄上的表现差异导致验证集虚高现象验证集准确率 0.97换到田里实拍图直接掉到 0.6 左右。原因公开数据集里很多病叶图片采集自同一批温室幼苗病害处于同一发病阶段比如都是早期褪绿斑点而田间成熟叶片上的病斑已经连片、枯黄、卷曲。模型学到的其实是“叶片亮绿色加小黑点”这种表面模式不是真正的病理特征。解决训练前按叶龄或发病阶段做分组把不同阶段图片切成 train/val/test而不是随机切分。切分后如果某个阶段类别不平衡尽量用增广补齐而不是硬着头皮训。5.2 背景干扰把土壤和手指数成了病斑现象Grad-CAM 可视化显示模型关注的是叶子边缘的泥土颗粒不是病斑。原因农作物病害公开数据集的采集背景不统一有些图在温室白背景拍的有些在田间泥土背景拍的模型学到的是“深色背景块 病斑”的捷径。这类短期准确率高换个背景就失效。解决训练增广里加RandomResizedCrop和RandomErasing人为遮挡部分背景同时用混合背景的数据集做预训练。如果模型仍然盯着背景返回数据阶段裁剪掉非叶片区域再训练。5.3 类别不平衡健康叶片远多于发病叶片现象整体准确率 0.9但查看每个类别的 precision 和 recall发病类别的 recall 只有 0.2。原因数据分布本身不均衡健康叶片容易采集发病叶片需要等病害发生才能拍到有些类别只有几十张。解决分类任务里用WeightedRandomSampler给少样本类别提高采样概率检测任务里把少样本类别单独复制到另一个目录做 oversampling但复制时要做轻微旋转和色调扰动直接复制会让模型记住同一张图的噪声。也可以用图像拼接的方式生成新的合成样本不过合成样本和真实样本的分布差异要单独评估。5.4 数据泄漏同一株作物的多张图片同时出现在训练和验证集现象验证集准确率 0.98训练集 loss 已经收敛但泛化很差测试集准确率反而掉到 0.8 以下。原因很多公开数据集采集时拍摄的是同一株作物的不同叶片这些图片虽然是不同文件但背景、光线、病害严重程度高度相似它们本质上属于同一个采样单元。随机切分时这些“副本”同时进了训练集和验证集验证指标虚高。解决按植株 ID 或拍摄 session 分组整个组只能进一个划分集合。数据集目录如果没有这些信息可以用图片的感知哈希聚类把相似度超过阈值的图片归为同一组再做组级划分。5.5 预训练权重和图像尺寸不匹配带来的玄学掉点现象用 640×640 输入训 YOLOv8参数量没变但比 416 输入掉了一个点且训练早期 loss 数值异常。原因预训练权重通常在固定分辨率下微调过直接换输入尺寸意味着模型要重新适配感受野和 anchor 分布。YOLOv8 是 anchor-free影响比老版本小但 BatchNorm 层的 running mean 和 running var 依然是预训练时的统计量换分辨率后前几个 epoch 会把它们“冲歪”。解决训练前先用--warmup参数让模型跑 3 到 5 个 epoch 的预热或者自己写一个用大学习率但小 batch 的预热阶段让 BatchNorm 统计量先适应新分辨率再恢复正常训练节奏。这个坑最玄但排查顺序应该排在数据问题之后。6. 进阶验证与上线习惯用类激活图找模型在看什么分类和检测模型训练完之后我多了一个固定动作用 Grad-CAM 检查模型在验证集上的注意力区域而不是只看准确率指标。这个习惯救过我一次——之前做一个水稻稻瘟病检测项目线上准确率 0.93但农艺师反馈说模型在分蘖期叶片上误报严重。我拿 Grad-CAM 一看模型关注的是叶片上的水滴反光不是病斑。如果把这一步放到训练之后、上线之前一次可视化就能省下两个星期的现场调试。用torchcam库做 Grad-CAM 很直接ResNet 的layer4是最后一个卷积特征图包含最丰富的语义信息from torchvision.models import resnet50 from torchcam.methods import GradCAM cam GradCAM(model, target_layerlayer4) model.eval() with torch.no_grad(): out model(img.unsqueeze(0)) # img 是单张经过 transform 的验证集图片 acts cam(out.squeeze(0).argmax().item(), out) cam.plot(acts[0])这段代码的关键是把预测类别传进 GradCAM它只对模型实际判定的那个类别做梯度回传你才能看到“模型为什么判成这个类”。如果传错了类别得到的激活区域没有任何诊断意义。cam.plot会把热力图叠加在原图上我习惯批量跑 20 张验证集图片把结果拼成一张图先扫一眼是否存在“关注背景”“关注叶缘”“关注水渍”这类系统性偏置。对于 YOLO 检测模型Grad-CAM 用起来麻烦一些我通常改用yolo val输出带预选框的可视化图重点看预测框和真实框的 IoU 分布。某个病害类别的 mAP 低于 0.5 时先统计该类别的误检是漏检还是错检漏检多说明框的尺寸先验有问题错检多说明存在相似类别混淆。这时候再看该类别的特征图比盲目调 anchor 或换模型有效得多。另一个值得养成的习惯是把每一次数据清洗动作记录下来删了多少张坏图、合并了哪些类、伪框修了多少个、越界框跳过了几条。这些数字在推理阶段遇到极端情况时能帮你快速判断是数据问题还是模型问题。最后说句实在话农作物病害数据集的坑大部分不在模型结构上而在标签语义和采集分布上。按这套流程走一遍虽然前期慢但至少模型上线后你不会慌。希望帮到你。本文还有配套的精品资源点击获取