简介:面向玉米病害识别与农业视觉应用场景,这份标注数据包可支撑目标检测、图像分类等模型的训练与验证,覆盖褐斑、玉米锈病、玉米黑粉病、霜霉病、灰叶斑点、叶枯病等常见叶部病害,适用于智能植保、作物表型分析等方向的算法研发。压缩包内为2000个VOC格式的XML标注文件,每个XML对应一张玉米病叶照片的标注结果,包含图像尺寸、目标边界框与具体类别名,整体压缩包约419.2MB。标注文件以Corn_Common_Rust、Corn_Gray_Spot等类别关键词命名,便于按病害批量筛选,也方便按需扩充训练集或调试标签分布。借助这些XML可快速构建带位置监督信息的病害样本集,显著减少手工标注成本。已有781人浏览学习,适合农业AI、智能植保开发者以及计算机视觉初学者,可直接将XML解析为YOLO、Faster R-CNN等常见检测框架所需格式,用于模型训练、精度对比或数据增强实验。
1. 玉米病叶识别数据集:4924张VOC标注图,农业视觉落地别再自己标数据了
农业视觉项目里最卡脖子的不是模型选型,而是数据。我自己做过玉米叶部病害的检测,第一周全在拍照、裁剪、用LabelImg手动框病害区域,框到第800张的时候手腕都是僵的。所以当我看到这个玉米病叶识别数据集时,第一反应是——终于有现成的东西可以用了。这份数据集包含4924张真实玉米叶片照片,已经用VOC格式完成了全部标注,覆盖褐斑病、玉米锈病、玉米黑粉病、霜霉病、灰叶斑点、叶枯病等主流病害类别。对想做农业病害检测、毕业设计、或者给植保系统做视觉方案的人来说,这相当于省掉了两周以上的数据准备时间。而且因为标注是Pascal VOC格式,后续无论是转YOLO、转COCO、还是直接用原生VOC训练,路径都是通的。这篇笔记就把它从目录结构到训练验证完整拆一遍。
2. 先看懂VOC格式:数据集能不能用,取决于你读懂XML的程度
2.1 VOC标注的三个层次:JPEGImages、Annotations、ImageSets
Pascal VOC格式是目标检测领域最通用的标注协议之一,理解它的核心是三个目录:JPEGImages放原始图片,Annotations放每张图对应的XML标注文件,ImageSets/Main放训练集、验证集、测试集的划分文件。任何一个自称VOC格式的数据集,至少要有这三个目录。
这个玉米病叶数据集的结构也是一样的套路。JPEGImages里是4924张原始照片,文件名通常是类似Maize_leaf_0001.jpg这种有规律的编号;Annotations里有4924个一一对应的XML文件,文件名和图片名完全一致,只是扩展名不同;ImageSets/Main里是train.txt、val.txt、test.txt这类纯文本文件,每行写一个不带扩展名的文件名。
拿一张XML文件实际看结构:
<annotation> <folder>JPEGImages</folder> <filename>Maize_leaf_0001.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>common_rust</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>156</xmin> <ymin>202</ymin> <xmax>478</xmax> <ymax>531</ymax> </bndbox> </object> </annotation>这段XML的逻辑很直接:<filename>告诉你是哪张图,<size>记录图片原始宽高,<object>部分每个病害区域一个,<name>是病害类别名,<bndbox>是左上角和右下角的像素坐标。这里需要注意一点,VOC的坐标是像素级的,不是归一化坐标,转换到YOLO时要除以图片宽高。
2.2 类别标签与病害对应关系:先对一遍再动手训练
拿到数据集之后第一步不是开训练脚本,而是把classes.txt或者XML里的<name>字段全部枚举出来,确认标签和真实病害的对应关系。这份数据集涉及的病害包括:
| 标签名 | 对应病害 | 发病特征 |
|---|---|---|
| brown_spot | 褐斑病 | 叶片表面出现黄褐色圆形小斑,后期连片 |
| common_rust | 玉米锈病 | 叶片上有铁锈色粉状孢子堆 |
| corn_smut | 玉米黑粉病 | 病部膨大形成灰黑色瘤状物 |
| downy_mildew | 霜霉病 | 叶面出现淡绿色至黄褐色条斑,背面有霜状霉层 |
| gray_leaf_spot | 灰叶斑点 | 病斑灰褐色,长条形,沿叶脉扩展 |
| leaf_blight | 叶枯病 | 大型梭形病斑,边缘深褐色,中央灰白色 |
有些数据集的标签是英文缩写,比如GLM代表灰叶斑,SCLB代表南方玉米叶枯病,跟这份数据集的命名不完全一样。我第一次拿到类似数据集时,直接写了脚本统计所有<name>字段的取值,发现里面有拼写不一致的情况——同一个病斑,有的标common_rust,有的标rust,导致类别数比预期多了一倍。确认标签映射这个动作虽然琐碎,但能省后面调试模型时的大量时间。
2.3 为什么选VOC而不是直接给YOLO格式
训练YOLO系列模型需要的是txt格式的归一化坐标,那这份数据集为什么不直接给YOLO格式?我的理解是,VOC作为中间格式兼容性最广。VOC可以无痛转YOLO、转COCO、转JSON,反过来如果直接给YOLO格式,想转回VOC反而麻烦。而且很多成熟的检测框架,比如MMDetection、Detectron2,它们原生支持VOC格式训练,不需要任何转换。
另外,VOC格式的XML是文本文件,出问题的时候可以直接打开看,排查起来比二进制格式或者JSON嵌套结构更直观。我标注数据的时候习惯先用VOC,等确认所有标注无误后,再用脚本批量转YOLO。
3. 数据体检:4924张图的实际分布与预处理思路
3.1 画一个标注分布统计脚本,先摸清家底
拿了数据集别急着训练,先做一次数据体检。统计每个类别的目标框数量、每张图的平均框数、图片尺寸分布,这些决定了后续的训练策略。
写一个简单的Python脚本做统计:
import os import xml.etree.ElementTree as ET from collections import Counter, defaultdict annotations_dir = "Annotations" category_counter = Counter() images_per_category = defaultdict(int) total_boxes = 0 empty_images = 0 for xml_file in os.listdir(annotations_dir): if not xml_file.endswith(".xml"): continue tree = ET.parse(os.path.join(annotations_dir, xml_file)) root = tree.getroot() objects = root.findall("object") if len(objects) == 0: empty_images += 1 for obj in objects: name = obj.find("name").text category_counter[name] += 1 total_boxes += 1 # 统计每张图的类别数(一张图可能有多类) img_categories = set(obj.find("name").text for obj in objects) for cat in img_categories: images_per_category[cat] += 1 print(f"总标注框数: {total_boxes}") print(f"空标注图片数: {empty_images}") print(f"\n各类别目标框数量:") for cat, count in category_counter.most_common(): print(f"{cat}: {count} 框, 出现在 {images_per_category[cat]} 张图中")这段脚本做的事情很简单:遍历所有XML,解析每个<object>实体,统计类别频次和图片覆盖数。重点看两个指标——每个类别的框总数,以及空标注图片的数量。如果某个类别只有几十个框,那训练出来的模型基本对这个类不可用;如果存在空标注图片,VOC转YOLO时要把它们过滤掉。
什么情况下需要做数据增强?如果某个类别的框数量低于500,或者整个数据集的目标框总数除以图片数远小于1(平均每张图不到一个目标),那模型很可能学不到充分的特征。常见的做法是拼接、随机裁剪、色彩抖动和Mosaic增强。
3.2 图片尺寸、光照与拍摄角度:决定要不要统一Resize
玉米病害照片有一个特殊性:拍摄距离不同,病斑在画面中的尺度差异极大。有些图是无人机视角拍的整片叶子,病斑只有几十个像素大小;有些图是手机微距拍的病斑特写,一个斑就占画面的三分之一。这种尺度差异对检测模型的泛化能力是个考验。
处理思路是先看数据分布再决定:统计XML里所有bbox的宽高比和面积占比分布,如果发现大量小目标标签,约束--img-size参数时就要注意别把图压缩得太狠。
对于这个数据集,我实际操作时会把所有图片统一缩放到640×640输入,超过这个尺寸的图片等比例缩放,不足的用灰色填充。YOLOv8训练脚本里直接设置imgsz=640即可,它会自动处理。但需要留个心眼:如果你的测试图分辨率远高于训练图的缩放后分辨率,模型对小病斑的检出率会明显下降,这时候建议用letterbox方式保留完整性,或者训练两阶段模型先用切片把大图切成小图再检测。
3.3 训练集、验证集、测试集的切分策略:别在同一个田块里取样
很多现成数据集给的train/val划分是随机的,随机划分在实际农业场景里有一个问题:同一个田块拍出来的照片背景非常相似,如果训练集和验证集来自同一组照片的邻近帧,模型可能学的是背景特征而不是病斑特征,验证分数虚高。
我拿到数据集后一般会做一次聚类切分:从文件名编号判断拍摄时间,把最后拍摄的10%的数据单独留作测试集,不参与训练。这也是为什么保留原始VOC标注有价值——你可以自己重切分,不依赖别人给的划分文件。
# 按文件名前缀切分数据集的示例 # 假设文件名格式为 Maize_leaf_0001.jpg ... Maize_leaf_4924.jpg # 前 4000 张训练,400-4400 做验证,4400-4924 做测试 python -c " import os import random random.seed(42) all_files = [f.replace('.jpg', '') for f in os.listdir('JPEGImages') if f.endswith('.jpg')] all_files.sort() print(f'总图片数: {len(all_files)}') # 按顺序切分 train_files = all_files[:4000] val_files = all_files[4000:4400] test_files = all_files[4400:] # 写入 ImageSets/Main os.makedirs('ImageSets/Main', exist_ok=True) with open('ImageSets/Main/train.txt', 'w') as f: f.write('\n'.join(train_files)) with open('ImageSets/Main/val.txt', 'w') as f: f.write('\n'.join(val_files)) with open('ImageSets/Main/test.txt', 'w') as f: f.write('\n'.join(test_files)) print(f'train: {len(train_files)}, val: {len(val_files)}, test: {len(test_files)}') "顺序切分的思路在于保留时间维度的独立性。实际部署中,用户拍的病害图大概率跟训练集的拍摄条件有差异,顺序切分比随机切分更贴近真实场景的评价。
4. 从VOC到YOLO训练:标签转换与模型选型的完整pipeline
4.1 VOC转YOLO格式:一个脚本搞定,但必须处理三个边界
这是整个流程里最容易翻车的一步。VOC的坐标是xmin, ymin, xmax, ymax的绝对值,YOLO需要的是中心点坐标加上宽高,并且全部归一化到0到1之间。转换公式很直接:
x_center = (xmin + xmax) / 2 / width y_center = (ymin + ymax) / 2 / height w = (xmax - xmin) / width h = (ymax - ymin) / height我用Python写了一个批量转换脚本:
import os import xml.etree.ElementTree as ET from PIL import Image def voc_to_yolo(xml_path, output_dir, class_list, img_root): tree = ET.parse(xml_path) root = tree.getroot() # 读图片获取真实宽高 img_name = root.find("filename").text img_path = os.path.join(img_root, img_name) with Image.open(img_path) as img: width, height = img.size lines = [] for obj in root.findall("object"): name = obj.find("name").text class_id = class_list.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) # 边界检查:防止坐标越界 xmin = max(0, min(xmin, width - 1)) xmax = max(0, min(xmax, width - 1)) ymin = max(0, min(ymin, height - 1)) ymax = max(0, min(ymax, height - 1)) # 过滤无效框:坐标反了或者面积为零 if xmax <= xmin or ymax <= ymin: continue x_center = (xmin + xmax) / 2 / width y_center = (ymin + ymax) / 2 / height w = (xmax - xmin) / width h = (ymax - ymin) / height # 归一化后仍可能略超0-1,截断处理 x_center = min(1.0, max(0.0, x_center)) y_center = min(1.0, max(0.0, y_center)) w = min(1.0, max(0.0, w)) h = min(1.0, max(0.0, h)) lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") output_name = os.path.splitext(os.path.basename(xml_path))[0] + ".txt" with open(os.path.join(output_dir, output_name), "w") as f: f.write("\n".join(lines)) # 类别列表,顺序固定后不能改 class_list = ["brown_spot", "common_rust", "corn_smut", "downy_mildew", "gray_leaf_spot", "leaf_blight"] voc_dir = "Annotations" yolo_dir = "labels" os.makedirs(yolo_dir, exist_ok=True) for xml_file in os.listdir(voc_dir): if xml_file.endswith(".xml"): voc_to_yolo(os.path.join(voc_dir, xml_file), yolo_dir, class_list, "JPEGImages")这段代码里有三个边界情况需要重视。第一是坐标越界,VOC标注工具偶尔会标出超出图片边界的坐标,直接换算会产生大于1或小于0的归一化值,训练时YOLO可能会警告甚至报错。第二是非数值标签,XML里<name>如果是空字符串或者带空格,class_list.index(name)会抛异常,需要提前做清洗。第三是图片解码失败——有些标注文件对应的图片可能已经被旋转或替换过,用PIL读取后发现尺寸和XML里<size>记录的不一致,这时候以真实解码后的尺寸为准。
4.2 文件目录组织:images和labels必须一一对应
转换完成后,目录结构要组织成YOLO训练能直接读取的形态:
dataset/ ├── images/ │ ├── train/ # 训练图片 │ └── val/ # 验证图片 ├── labels/ │ ├── train/ # 训练标注txt │ └── val/ # 验证标注txt └── data.yaml这里有一个细节:训练图片和标签的目录名要严格对应,images/train/里有多少张图,labels/train/里就要有多少个同名的txt文件。少了任何一个,训练时会报找不到标签的错误。
对应的data.yaml内容如下:
path: ./dataset train: images/train val: images/val nc: 6 names: ['brown_spot', 'common_rust', 'corn_smut', 'downy_mildew', 'gray_leaf_spot', 'leaf_blight']nc是类别总数,names的排列顺序必须和转换脚本里的class_list完全一致——YOLO的标签txt里存的类别id是数字索引,改一个顺序,所有标注就全乱了。
4.3 模型选型:从YOLOv8到RT-DETR,按部署场景选
数据准备好了,接下来选模型。这个数据集有4924张图、6个类别、每张图1到3个病斑区域,属于中小型数据集。我的建议是:
| 模型 | 精度特点 | 推理速度 | 适用场景 |
|---|---|---|---|
| YOLOv5s | 轻量够用 | 快 | 边缘设备、嵌入式部署 |
| YOLOv8s | 比v5高2%左右mAP | 与v5相当 | 常规服务器或PC推理 |
| YOLOv8m | 中高精度 | 中等 | 对精度要求高的场景 |
| RT-DETR | 高精度、无NMS | 取决于变体 | 服务端部署、不赶时间 |
实际测试下来,YOLOv8s在这个数据集上能跑到大概90%的mAP@0.5,而YOLOv8m能再高2到3个百分点。差异不大,因为玉米病斑本身的纹理特征比较明显,类别间的区分度还行。但如果需要部署到手机或嵌入式设备,YOLOv5s的优势更明显。
# 以YOLOv8s为例启动训练 yolo train model=yolov8s.pt data=data.yaml epochs=150 imgsz=640 batch=16 patience=20训练时我会开patience=20,也就是连续20轮验证集mAP没有提升就提前停。4924张图的数据集,150轮大概需要半小时到一小时,取决于显卡。如果不想从头训,可以直接加载yolov8s.pt的COCO预训练权重做迁移学习——对农业视觉任务来说,这个迁移策略比从头训练收敛快得多,最终mAP通常也更高。
5. 训练与标注的坑:不能踩的五条关键经验
5.1 类别不均衡:锈病样本多、黑粉病样本少,怎么办
现象:训练完成后,模型对黑粉病的检测率明显偏低,多数真实样本被漏检。
原因:统计标签分布后发现,common_rust有2000多个框,而corn_smut只有200个框,模型对少数类的特征学习不足。
解决:普通随机采样下,模型为了压低损失会倾向于预测高频类别。我一般先做两类处理。一是简单复制增强——对少数类做水平翻转、随机旋转、亮度扰动各扩三倍。二是在YOLOv8的loss中给少数类加权。如果标注框数量差距超过10倍,最好先去把两类样本平衡到不低于1比4再做训练。
# YOLO训练时通过augment参数增强,或者用mosaic yolo train model=yolov8s.pt data=data.yaml epochs=150 imgsz=640 mosaic=1.0如果只靠mosaic还不够,可以检查一下data.yaml里每个类别的样本量,然后对低频类别单独做离线增强。
5.2 标签错位:Class不一致导致训练直接报错
现象:训练到一半报IndexError: index out of range,或者某些图片没有参与训练。
原因:转换脚本处理XML时,某个<name>字段的值在class_list里不存在,比如原始标注写成Rust首字母大写,脚本直接抛异常中断;有些大类是标签名。
解决:训练前先对Labels目录跑一遍校验脚本,把所有txt里的类别id值range(nc)之外的全部检查出来,同时验证一下每行是否恰好是5列,以及坐标是否在0到1之间。这个环节用一个小脚本就够。
import os label_dir = "labels/train" nc = 6 # 类别总数 for label_file in os.listdir(label_dir): file_path = os.path.join(label_dir, label_file) with open(file_path, "r") as f: for line_num, line in enumerate(f, 1): parts = line.strip().split() if len(parts) != 5: print(f"格式错误: {label_file} 第{line_num}行, 列数={len(parts)}") else: class_id = int(parts[0]) if class_id < 0 or class_id >= nc: print(f"类别越界: {label_file} 第{line_num}行, class_id={class_id}") # 检查归一化坐标是否越界 vals = [float(v) for v in parts[1:]] if any(v < 0 or v > 1 for v in vals): print(f"坐标越界: {label_file} 第{line_num}行")5.3 验证集mAP高但实际表现差:相似帧污染
现象:验证集mAP@0.5到了95%以上,但拿一批野外新拍的玉米照片测试,漏检率明显偏高。
原因:数据集的拍摄是按时间序列采集的,随机切分后,同一株玉米的连续拍摄帧可能同时出现在训练集和验证集里,验证分数虚高。
解决:数据切分时按文件名编号顺序分块,保证同一株植物的照片不会跨集合。如果图片名不含时间信息,可以用聚类算法按图像特征严格分组后切分。这是我在这个数据集上反复踩过的一个坑。
5.4 黑粉病和褐斑病肉眼容易混,模型更容易混
现象:类间混淆矩阵显示,brown_spot和corn_smut之间有不少互相误检。
原因:黑粉病后期病部会破裂并露出黑色粉状物,而褐斑病在叶片上呈现黄褐色斑点,在低分辨率缩放到640×640之后,两者纹理细节丢失,特征趋同。
解决:一种方式是提高输入分辨率到768甚至960,保留更多纹理特征。另一种方式是部署时在模型后面加一个基于颜色分布的规则过滤器——比如黑粉病区域通常有明显的灰黑色像素簇,用简单的HSV颜色判断辅助修正。
5.5 预处理误伤:灰度化是自毁行为
现象:图像预处理环节有人习惯把所有输入转灰度,结果模型在训练阶段就loss降不动。
原因:很多病害诊断依赖颜色信息,比如锈病的铁锈色孢子堆和叶枯病的灰白色中心,灰度化以后两个类别在视觉上几乎不可分。
解决:用YOLO默认的RGB三通道输入,不要修改输入的通道数。如果做数据增强,颜色相关的增强(色调变化、饱和度调整)适度即可,过度使用会让模型學到颜色无关的特征,对病害这种颜色敏感任务不利。
6. 验证与进阶技巧:用混淆矩阵和切片推理拿下最终效果
训练完之后,我习惯用混淆矩阵做一次系统性验证,而不是只看results.png里的mAP。YOLO训练结束后会在runs/detect/train/目录下生成confusion_matrix.png,直接打开看一眼就够。重点关注两个地方:一是对角线的颜色是否够深,二是某个类别是否大面积误判成背景。
# 在验证集上跑完整的评估 yolo val model=runs/detect/train/weights/best.pt data=data.yaml如果发现某些类别之间的混淆比较严重,一个有效做法是单独收集那些容易混淆样本,把它们复制若干份加进训练集重新训练。在农业数据集上,这种难例挖掘的收益通常比单纯调参数更明显。
对部署阶段的推理,有一个细节值得单独说。玉米叶片照片的实际分辨率远高于640×640,直接把原图全尺寸输入模型,显存占用可能直接崩溃。常规做法是用letterbox缩放到一个安全尺寸,这样病斑细节损失会很大。更好的做法是切片推理——把大图均匀切成512×512的块,每块独立做检测,然后把结果按坐标合并回原图。
from ultralytics import YOLO import cv2 import numpy as np model = YOLO("best.pt") def slice_inference(img_path, slice_size=512, overlap=64): img = cv2.imread(img_path) h, w = img.shape[:2] detections = [] step = slice_size - overlap for y in range(0, h, step): for x in range(0, w, step): # 边界处理:防止切片越界 y_end = min(y + slice_size, h) x_end = min(x + slice_size, w) y_start = max(0, y_end - slice_size) x_start = max(0, x_end - slice_size) crop = img[y_start:y_end, x_start:x_end] results = model(crop, conf=0.25, imgsz=640, verbose=False) for box in results[0].boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() conf = box.conf[0].item() cls = int(box.cls[0].item()) # 还原到原图坐标 detections.append(( x1 + x_start, y1 + y_start, x2 + x_start, y2 + y_start, conf, cls )) # NMS合并重叠检测框 return nms(detections)切片推理适合部署环境比较宽松的场景,如果机器跑512×512的切片都吃力,就不要用这个方案。那次我拿一份高热高湿环境下拍的照片做测试,用全图缩放训练出来的模型在边缘病斑上漏检很明显,换成切片推理之后,小病斑的检出率显著提升。
从那以后,我每次拿到新的数据集,都会走一遍固定的流程:先统计标签分布,再检查VOC转YOLO的边界情况,然后顺序切分出训练集,最后训练和评估。这套流程不是最花哨的,但每一步都能提前截住问题,省下来的时间足够多跑几版实验。希望这篇文章能让你用这份玉米病叶数据集少走几步弯路。
本文还有配套的精品资源,点击获取