简介:本资源为玉米叶部病害识别数据集,面向从事农业图像分类、目标检测的算法工程师与高校研究者,可用于训练和验证玉米叶枯病、普通锈病、灰叶斑病及健康叶片的识别模型。数据采用VOC格式人工标注,共10884张图片,压缩包内包含2000个XML标注文件,整体约520.9MB,标注信息与图像一一对应,便于直接接入YOLO、Faster R-CNN等主流检测框架。已有377人学习下载,适合需要快速构建玉米病害识别基线或扩充训练样本的读者。借助该数据集,可完成从数据加载、类别平衡到模型训练与精度评估的完整流程,参考标注准确率可达95.7%以上,为病害早期诊断与田间智能监测提供可靠的数据支撑。
1. 玉米叶病预测数据集:10884 张 VOC 标注图,为什么值得先跑一遍再谈模型
如果你正在做作物病害识别,大概率遇到过这种局面:网上能找到的玉米叶病数据要么只有几百张、类别残缺,要么标注格式是分类文件夹,想训检测模型还得自己画框。这个数据集直接给到 10884 张图片,全部按 VOC 格式人工标注,覆盖叶枯病、普通锈病、灰叶斑病和健康叶片四类,官方给出的准确率参考值在 95.7% 以上。它解决的不是“有没有数据”的问题,而是“标注质量能不能直接进训练管线”的问题。适合两类人:一类是想快速验证检测模型效果、不想在数据清洗上耗两周的算法工程师;另一类是拿它做课程设计或论文实验、需要一份格式规范、类别清晰的数据底座的开发者。VOC 格式意味着每张图对应一个 XML,框坐标、类别名、图像尺寸都在里面,转 YOLO、COCO 或者直接喂给 SSD、Faster R-CNN 都有成熟脚本可走,省掉的是最枯燥的那段手工活。
2. VOC 标注结构拆解:10884 张图到底怎么组织、怎么读
2.1 目录结构与 XML 字段含义
拿到压缩包后,常见做法是先别急着写 DataLoader,而是把目录树打印出来看一眼。VOC 风格的数据集通常长这样:
# 查看数据集目录结构(示例,实际以压缩包内为准) tree -L 2 ./corn_leaf_voc/ # 输出大致为: # corn_leaf_voc/ # ├── Annotations/ # 存放所有 .xml 标注文件 # ├── JPEGImages/ # 存放所有 .jpg 原图 # ├── ImageSets/ # │ └── Main/ # 存放 train.txt / val.txt / test.txt 划分文件 # └── SegmentationClass/ (可选,本数据集以检测为主,可能不存在)Annotations和JPEGImages是核心。每张图xxx.jpg对应一个同名xxx.xml。XML 里几个字段必须盯住:<size>下的width、height决定坐标是否越界;<object>下的<name>是类别字符串,直接决定你后面类别映射表怎么写;<bndbox>里的xmin/ymin/xmax/ymax是左上角和右下角绝对坐标,不是归一化值。人工标注的数据集最容易出问题的地方就在bndbox:框超出图像边界、宽高为 0、坐标写成浮点数。先写个脚本扫一遍,比训到一半 loss 不降再回头查要省事得多。
import os import xml.etree.ElementTree as ET from collections import Counter ann_dir = "./corn_leaf_voc/Annotations" img_dir = "./corn_leaf_voc/JPEGImages" class_counter = Counter() bad_boxes = [] for xml_file in os.listdir(ann_dir): if not xml_file.endswith(".xml"): continue tree = ET.parse(os.path.join(ann_dir, xml_file)) root = tree.getroot() size = root.find("size") w = int(size.find("width").text) h = int(size.find("height").text) for obj in root.findall("object"): name = obj.find("name").text.strip() class_counter[name] += 1 box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(box.find("ymax").text) # 检查越界和无效框 if xmin < 0 or ymin < 0 or xmax > w or ymax > h or xmax <= xmin or ymax <= ymin: bad_boxes.append((xml_file, name, xmin, ymin, xmax, ymax)) print("类别分布:", class_counter) print("异常框数量:", len(bad_boxes)) for item in bad_boxes[:10]: print(item)这段脚本做三件事:统计四类目标的框数量,判断类别是否均衡;检查坐标是否越界或宽高非法;把异常样本路径打出来。参数上唯一需要改的是ann_dir和img_dir两个路径。如果bad_boxes数量在几十以内,可以直接在转格式时过滤掉;如果上百,说明标注环节有系统性问题,得考虑是否重新清洗或只取子集训练。类别分布那行输出尤其重要——健康叶片如果占比过高,后面训练时分类头会偏向多数类,mAP 看着还行但病害漏检会很难看。
2.2 从 VOC 到 YOLO:转换脚本与类别映射
VOC 转 YOLO 是这套数据最常走的一步。YOLO 要求每张图一个.txt,每行class_id x_center y_center width height,全部归一化到 0~1。类别映射必须固定顺序,否则训练和推理对不上。
import os import xml.etree.ElementTree as ET # 类别映射:顺序一旦确定,训练和推理必须一致 classes = ["叶枯病", "普通锈病", "灰叶斑病", "健康"] class_to_id = {name: idx for idx, name in enumerate(classes)} ann_dir = "./corn_leaf_voc/Annotations" label_out = "./corn_leaf_yolo/labels" img_out = "./corn_leaf_yolo/images" os.makedirs(label_out, exist_ok=True) os.makedirs(img_out, exist_ok=True) for xml_file in os.listdir(ann_dir): if not xml_file.endswith(".xml"): continue tree = ET.parse(os.path.join(ann_dir, xml_file)) root = tree.getroot() size = root.find("size") w = int(size.find("width").text) h = int(size.find("height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text.strip() if name not in class_to_id: continue # 跳过未定义类别,避免训练时索引错位 cls_id = class_to_id[name] box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(box.find("ymax").text) # 归一化并限制在 [0,1] x_center = min(max((xmin + xmax) / 2.0 / w, 0.0), 1.0) y_center = min(max((ymin + ymax) / 2.0 / h, 0.0), 1.0) bw = min(max((xmax - xmin) / w, 0.0), 1.0) bh = min(max((ymax - ymin) / h, 0.0), 1.0) lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {bw:.6f} {bh:.6f}") if lines: txt_name = xml_file.replace(".xml", ".txt") with open(os.path.join(label_out, txt_name), "w") as f: f.write("\n".join(lines)) # 图片软链或复制到 YOLO 目录,这里用软链省空间 img_name = xml_file.replace(".xml", ".jpg") src = os.path.join("./corn_leaf_voc/JPEGImages", img_name) dst = os.path.join(img_out, img_name) if os.path.exists(src) and not os.path.exists(dst): os.symlink(os.path.abspath(src), dst)逻辑说明:先按固定classes列表建映射,遇到不在表里的类别直接跳过,防止后面cls_id越界。归一化时用min/max夹到 0~1,处理掉少量越界框。图片用软链而不是复制,10884 张图如果每张几 MB,复制一份会多占几十 GB,软链在 Linux 下训练读取没问题,Windows 下建议改成shutil.copy。参数上,classes顺序可以改,但改完必须同步改 YOLO 的.yaml配置文件里的names,否则训练出来的模型会把叶枯病认成健康。转完后抽查几个.txt,用cat看一眼数值是否在 0~1 之间,这是最便宜的验证。
3. 训练管线搭建:从划分数据集到跑通第一个 baseline
3.1 训练集/验证集/测试集划分
VOC 自带的ImageSets/Main不一定有现成划分,常见做法是自己按 8:1:1 切。注意不能随机切完就完事——同一片叶子可能被拍了好几张,如果这些图分散到训练和验证集,验证指标会虚高。更稳的做法是按“拍摄批次”或“文件前缀”分组切,但数据集没给批次信息时,至少用固定随机种子切一次,保证可复现。
import os import random random.seed(42) # 固定种子,保证每次划分一致 all_files = [f.replace(".xml", "") for f in os.listdir("./corn_leaf_voc/Annotations") if f.endswith(".xml")] random.shuffle(all_files) n = len(all_files) train_end = int(n * 0.8) val_end = int(n * 0.9) splits = { "train": all_files[:train_end], "val": all_files[train_end:val_end], "test": all_files[val_end:] } for split, files in splits.items(): with open(f"./corn_leaf_yolo/{split}.txt", "w") as f: for name in files: f.write(f"./images/{name}.jpg\n") print(split, len(files))参数说明:random.seed(42)是后悔药,换种子会得到不同划分,指标波动可能到一两个点,论文实验里必须固定。0.8/0.1/0.1是检测任务的常见比例,如果数据量少可以调成 7:2:1。输出文件里写的是相对路径,YOLO 训练时从data.yaml里的path根目录开始找,路径写错会直接报No such file。
3.2 YOLO 配置文件与启动命令
以 YOLOv8 为例,先写corn_leaf.yaml:
# corn_leaf.yaml path: ./corn_leaf_yolo train: train.txt val: val.txt test: test.txt names: 0: 叶枯病 1: 普通锈病 2: 灰叶斑病 3: 健康names的顺序必须和转换脚本里的classes完全一致,差一个位置,训练时标签就全错。启动训练:
yolo detect train \ data=corn_leaf.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20 \ project=runs/corn_leaf \ name=baseline参数逐个说:model=yolov8n.pt是最小模型,先跑通再换s或m;imgsz=640是检测常用输入尺寸,如果显存不够降到 512,但小病斑可能被缩没;batch=16在 8GB 显存上比较稳,爆显存就减半;lr0=0.01是初始学习率,YOLO 默认值,数据量上万时一般不用大改;patience=20表示 20 轮验证指标不升就早停,省时间。跑完后看runs/corn_leaf/baseline/results.csv,重点盯metrics/mAP50-95和每类的metrics/mAP50。如果健康类很高但灰叶斑病很低,说明该类样本少或病斑小,后面再针对性补数据或调 anchor。
4. 避坑与排查:标注、转换、训练里最容易翻车的五件事
4.1 类别名带空格或大小写不一致
现象:转换脚本跑完,class_counter里出现“叶枯病”和“叶枯病 ”两个键,或者“健康”和“healthy”混用。原因:人工标注时不同人输入习惯不同,XML 里<name>文本没统一。解决:在读取时统一strip()并做一次映射归一化,比如把所有别名指向同一个标准名,再写进class_to_id。这个坑不查,训练时类别数会莫名多出来,模型学得四不像。
4.2 图片和 XML 文件名对不上
现象:转 YOLO 时提示某张图找不到,或者生成的.txt没有对应图片。原因:VOC 数据集常见.JPG和.jpg混用,或者 XML 有但图片缺失。解决:转换前先做一次集合比对,把Annotations里的文件名去掉后缀和JPEGImages里的做交集,只保留两边都有的样本。缺失的图片直接丢弃对应 XML,别硬凑。
4.3 框坐标越界导致 loss 变 NaN
现象:训练几个 epoch 后 loss 突然变nan。原因:个别bndbox的xmax大于图像宽度,归一化后宽高为负,YOLO 计算 IoU 时出问题。解决:转换脚本里已经用min/max夹住,但更彻底的做法是在 2.1 的扫描脚本里把异常框直接过滤,不写进.txt。如果已经开训,回滚到上一个 checkpoint,清洗后重跑。
4.4 验证集指标虚高
现象:验证集 mAP 到 0.98,测试集一跑掉到 0.85。原因:划分时同一片叶子的多张图被分到训练和验证两边,模型见过近似样本。解决:按文件前缀或拍摄时间分组切分,确保同一组图只出现在一个 split 里。没有分组信息时,至少把random.seed固定并记录,换种子多跑几次看波动范围。
4.5 显存不足误判为模型问题
现象:batch=16时报 CUDA out of memory,改成batch=8后训练正常但指标下降。原因:不是模型不行,是 batch 小了 BN 统计不稳。解决:优先用梯度累积模拟大 batch,或者换更小的输入尺寸imgsz=512,再不行才换yolov8n。别一报错就怀疑数据,先看显存占用和nvidia-smi。
5. 进阶技巧:用 95.7% 这个参考值反推你的模型差在哪
官方给的 95.7% 以上准确率是个锚点,但别直接拿它当及格线——不同模型、不同输入尺寸、不同划分方式都会让数字漂移。我一般会做三件事来定位差距。第一,按类别拆开看 mAP。如果叶枯病和普通锈病都到 0.95 以上,灰叶斑病只有 0.8,那问题在灰叶斑病样本少或病斑小,可以针对性做 mosaic 增强或复制该类样本。第二,看混淆矩阵。健康叶片被误判成灰叶斑病,通常是背景纹理相似,加一些负样本或调cls损失权重。第三,用测试集跑一次yolo detect val,把conf阈值从 0.25 调到 0.4 再跑,看 mAP 变化。如果掉得厉害,说明模型置信度分布散,需要更多数据或更长训练。
# 用不同置信度阈值验证,观察 mAP 稳定性 yolo detect val \ model=runs/corn_leaf/baseline/weights/best.pt \ data=corn_leaf.yaml \ split=test \ conf=0.4 \ iou=0.6conf=0.4比默认 0.25 更严,iou=0.6是 NMS 阈值。如果conf=0.4时 mAP 只掉一两个点,说明模型输出比较自信,可以上线;如果掉十个点以上,回去补数据或加训。还有一个习惯:每次改完数据或配置,先跑 10 个 epoch 的小实验看趋势,别一上来就 300 epoch,电费和时间的血泪经验。从那以后我每次拿到新数据集,都强制先跑一遍标注扫描和类别分布,再动训练脚本。希望帮到你。
本文还有配套的精品资源,点击获取