29类叶片病害目标检测:VOC格式数据集解析、清洗与训练前校验全流程
简介面向计算机视觉目标检测入门与进阶学习者这份数据集覆盖29种常见植物叶片病害包含葡萄叶黑腐病、番茄叶菌斑、苹果锈叶病、马铃薯晚疫病等典型类别。资源按VOC标注格式组织data目录下分为训练集与验证集其中训练集包含2345张图片及对应xml标注文件验证集含239张图片及xml文件训练与验证数据均已做好划分可直接用于模型训练无需额外处理。包内还提供类别标签json字典文件便于在训练时加载类别映射附带可视化Python脚本随机传入一张图片即可绘制并保存边界框标注结果方便快速预览标注质量。压缩包共2000个文件以xml标注文件为主包含1个Python脚本包体约934.99MB。已有255人学习浏览适合目标检测方向课程设计、毕业设计或算法实践参考。1. 目标检测数据集怎么选叶片病害29类为什么用VOC格式打底植物叶片病害检测是农业视觉里落地难度比较高的场景。病害区域边界不像行人或车牌那样清晰叶斑、锈病、霜霉的颜色纹理与健康组织高度接近标注员自己都可能犹豫框线该往哪里收。标题给的是一个能直接开工的起点29类植物叶片病害缺陷检测数据集训练集和验证集已经分好标注采用VOC格式附带类别json文件和可视化脚本。适合做迁移学习、YOLO系列训练、MMDetection对比实验的工程师拿到就能跑通数据加载流程不必从零采集和清洗。VOC格式在当下不算新潮但兼容性最好几乎所有检测框架都保留转换入口后续切YOLO格式或者COCO格式都是一个脚本的事。不管用什么框架训练自己的数据集前期把标注格式吃透后面返工最少。2. 拆解VOC标注格式从目录结构到XML标签把29类病害映射到位VOC标注格式的核心是以图片为单位一张原图配一个同名XML文件XML里记录图像尺寸、通道数和每个目标的类别名与边界框坐标。标题里同时给出类别json文件说明这个数据集的类别体系以json为权威基准XML里的name字段需要能在json覆盖的29个类别中一一对上不能有多余拼写。文本检查只能保证语法正确真正发现问题要靠统计和渲染这两步分别放在第3章和第5章处理。2.1 目录结构与XML字段的对应关系拿到数据包先看目录组织。常见做法有两种一种是严格按Pascal VOC惯例组织另一种是简化为train和val两个文件夹各存图片和XML。标题说“包含训练集和验证集”对应到VOC惯例就是ImageSets/Main下的train.txt和val.txt每个txt一行一个不带扩展名的文件名。先看一眼根目录结构再决定训练脚本的路径写法避免把路径写死之后换机器还要改代码。plant_disease_dataset/ ├── JPEGImages/ # 原图jpg或png │ ├── leaf_0001.jpg │ └── ... ├── Annotations/ # 与JPEGImages同名的XML │ ├── leaf_0001.xml │ └── ... ├── ImageSets/ │ └── Main/ │ ├── train.txt │ └── val.txt └── classes.json # 29个类别名与id的映射目录结构确定后打开一个XML样例确认字段写法。不同标注工具导出的VOC格式在细节上有差异有的在bndbox里存整数有的存浮点有的object下额外带difficult和pose字段。叶片病害数据集的XML一般比较朴素重点关注size和object两块。annotation folderJPEGImages/folder filenameleaf_0001.jpg/filename size width1280/width height720/height depth3/depth /size object nameapple_scab/name bndbox xmin112/xmin ymin87/ymin xmax534/xmax ymax612/ymax /bndbox /object /annotationXML层级不深关键是name和bndbox。name对应29类中的一类bndbox四个坐标是xmin、ymin、xmax、ymax单位是像素供模型训练前转成归一化坐标使用。一张图上可以有多个object同一个类别也可以出现多次遍历时不能只读取第一个节点。表VOC XML核心字段说明字段含义解析注意事项filename图片文件名与JPEGImages目录实际文件名严格一致size/width、size/height图像宽高目标框越界判断依据object/name类别名必须在classes.json的29类范围内object/bndbox边界框坐标xminxmax、yminymaxobject/difficult难例标记部分XML无此字段读取时给默认值2.2 用ElementTree解析29类标注的通用函数解析VOC XML不需要引入大型依赖Python标准库xml.etree.ElementTree足够。把解析逻辑封装成独立模块的好处是后面的可视化脚本、数据清洗脚本、类别统计脚本都要复用它功能一致时只维护一份代码。坐标统一存成float转YOLO格式时直接做除法不用来回转换类型。import xml.etree.ElementTree as ET def parse_voc_xml(xml_path): tree ET.parse(xml_path) root tree.getroot() info { filename: root.findtext(filename), width: int(root.findtext(size/width)), height: int(root.findtext(size/height)), depth: int(root.findtext(size/depth)), objects: [], } for obj in root.iter(object): name obj.findtext(name) xmin float(obj.findtext(bndbox/xmin)) ymin float(obj.findtext(bndbox/ymin)) xmax float(obj.findtext(bndbox/xmax)) ymax float(obj.findtext(bndbox/ymax)) difficult int(obj.findtext(difficult, 0)) info[objects].append({ name: name, bbox: [xmin, ymin, xmax, ymax], difficult: difficult, }) return infofindtext支持类似bndbox/xmin的路径表达式比连续调用find再find简洁也少一层空引用判断。difficult的缺省值写为“0”兼容不输出该标签的标注工具。遍历使用root.iter(object)而不是root.findall(object)iter会递归查找所有层级的object节点即便标注工具额外包了一层组节点也不会漏掉。解析结果统一成dict结构字段名固定。这样后面无论是做train/val划分检查还是按类别过滤和重映射传入的都是同一结构。把这段函数保存为voc_utils.py后续所有脚本都从这里import需要改解析规则时只改一处。2.3 从XML到训练清单划分文件的生成与检查如果拿到的数据集只给了train和val两个目录没有单独的txt划分文件训练时需要自己生成。反过来如果给的是标准VOC目录也要核对txt里的每一行是否都能找到对应的图片和XML缺一个文件整个训练都会中断。用一个脚本把txt生成和校验合并完成。from pathlib import Path def write_split_txt(split_txt, xml_names): Path(split_txt).write_text(\n.join(xml_names) \n) train_names sorted(p.stem for p in Path(Annotations_train).glob(*.xml)) write_split_txt(ImageSets/Main/train.txt, train_names)这个脚本把Annotations_train目录下全部XML按文件名去扩展名排序写入train.txt。如果你的数据是简化目录结构用同样的方式处理val目录即可。如果数据集原本就有划分文件不要直接覆盖先备份原txt再手动比对差异更稳妥的划分方式是先把图片按叶片的植株编号分组再按组切分避免同一株叶片的图同时出现在训练集和验证集里指标会显得虚高。3. 用可视化脚本检查标注质量训练集验证集划分一眼看出问题文本解析只能确认XML结构完整真正能看出标注质量的是渲染结果。框是否紧贴病斑、类别名是否张冠李戴、有没有把整片叶子框进去却漏掉真正的病斑这些必须对着图看。数据集自带的可视化脚本价值在这里但拿到脚本后不要直接跑全量先看它读的是VOC标准目录还是简化目录再看它输出到哪里最后确认坐标系没有被resize打乱。3.1 渲染脚本的基本绘制与参数复用第2章的parse_voc_xml配合OpenCV画框和标签。核心函数控制在二十行左右入参是图片路径、XML路径、类别颜色表和输出路径。类别颜色表从classes.json生成保证同一个类别在所有图上颜色一致抽查时对比多张图能快速发现同类别的框风格漂移。import cv2 from pathlib import Path def render_annotation(image_path, xml_path, class_colors, output_path): img cv2.imread(str(image_path)) if img is None: print(ffailed to read image: {image_path}) return False ann parse_voc_xml(xml_path) for obj in ann[objects]: xmin, ymin, xmax, ymax [int(v) for v in obj[bbox]] cls obj[name] color class_colors.get(cls, (0, 255, 0)) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), color, 2) label cls cv2.putText(img, label, (xmin, max(ymin - 6, 0)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) cv2.imwrite(str(output_path), img) return True绘制参数里比较重要的是线宽、字体和标签位置。分辨率1280以上的大图线宽取2比较合适缩略图里仍能看清ymin - 6把文字顶到框线上方同时用max限制最小值避免目标贴顶时文字画出图像外。OpenCV画中文标签会乱码类别名建议保持英文或拼音classes.json里的类名也尽量用英文键。标签和框线重叠会遮住病斑如果类别名较长可以把文字画到框的左上角内部并垫一个半透明矩形。表可视化脚本常用绘制参数与调整建议参数建议值调整场景框线宽2小图或缩略图拼图时改为1字体大小0.6框特别小时改0.4避免文字占满框颜色策略类别固定色相邻类视觉相近时换高区分度色卡输出格式jpg需要透明背景看重叠时改png批量渲染时按ImageSets/Main里的txt清单读取而不是遍历Annotations目录这样能保证训练集和验证集分开输出也顺便验证了txt里每个文件名都能找到对应的图和XML。遇到文件名找不到的情况不要跳过把缺失路径写进日志这是划分文件不齐的最直接信号。3.2 训练集和验证集划分的视觉核对划分检查的重点不在列表文件本身而在图像内容。train和val如果来自同一个采集批次病斑形态、光照、叶片角度高度相似验证集指标会虚高。把两个txt的前几十个文件名渲染出来并排看整体风格有没有差异再统计两个集合的类别分布确认29个类在验证集里都有样本某个类在val里一个框都没有mAP算出来的该类指标是无效的。python render_annotation.py \ --images JPEGImages \ --annotations Annotations \ --list ImageSets/Main/train.txt \ --output preview_train.jpg python render_annotation.py \ --images JPEGImages \ --annotations Annotations \ --list ImageSets/Main/val.txt \ --output preview_val.jpg命令行参数比硬编码路径更适合这个场景因为训练集和验证集要反复切换。--images指向原图目录--annotations指向XML目录--list指向划分文件输出大图用--output指定。脚本内部从list逐行读文件名拼出完整路径再调用render_annotation一行缺失立刻能看出来。对比两个预览图时先看整体背景是否一致再看同类别病斑在两个集合中的表现差异。如果val里大多数图是单一背景、病斑大而清晰而train里背景杂乱、病斑小而密集说明验证集选得太容易模型实际落地能力会被高估。如果出现这种情况宁可手动从train里匀一部分多样本图片到val也不能只信mAP数值。验证集的构建标准要和业务使用场景对齐这一点比训练集更敏感。提示可视化输出不要直接删除。训练结束后用同一套渲染脚本出推理对比图排查漏检和误检时能省很多时间。4. 模型训练前的数据校验类别json的加载、重映射与脏数据清洗classes.json是连接VOC标注和训练框架的桥梁。XML里存的是类别名训练代码需要的是类别id到类名的映射而且不同框架要求的json结构不一样。有的框架读list有的读dict有的要求id连续从0开始。写一个兼容多结构的加载函数后续切YOLOv8、MMDetection还是Torchvision都不用改解析逻辑。4.1 类别json的三种结构统一加载import json def load_class_map(json_path): with open(json_path, r, encodingutf-8) as f: raw json.load(f) if isinstance(raw, list): id_to_name {idx: name for idx, name in enumerate(raw)} elif isinstance(raw, dict): keys list(raw.keys()) if all(k.isdigit() for k in keys): id_to_name {int(k): v for k, v in raw.items()} else: name_to_id raw id_to_name {v: k for k, v in name_to_id.items()} else: raise ValueError(unsupported classes json format) name_to_id {v: k for k, v in id_to_name.items()} return id_to_name, name_to_id这个函数兼容三种常见写法list形式、字符串数字做key的dict、类别名做key的dict。返回两个方向都能查的映射表name_to_id用于把XML里的name转成训练标签id。注意dict遍历顺序在Python3.7后是插入序如果json里的id不是连续整数list形式会丢失原始id语义优先用dict形式。加载后顺手打印len(id_to_name)确认正好是29类如果多出空串或者重复类名先修json再往下走。映射确定后重映射是常见操作。例如只用其中几类做实验或者把语义相近的类合并比如把两种褐斑病归成一类。重映射比重新标注成本低得多关键是把旧类名转换成新类名或新id时要保留一份独立的val划分否则类别合并后验证集会跟着变指标失去对比意义。def remap_classes(xml_info, mapping): new_objects [] for obj in xml_info[objects]: new_name mapping.get(obj[name]) if new_name is not None: new_objects.append({**obj, name: new_name}) xml_info[objects] new_objects return xml_infomapping的形式是{旧类名: 新类名}不在mapping里的类直接丢弃。这个函数返回新的dict结构不修改原XML方便做A/B实验时来回切换。合并类别后还需要重新统计每类的框数量确认没有产生空类否则训练框架会在配置文件里报类别数不一致。解析和重映射之后最常见的落地下一步是把VOC坐标转成YOLO训练用的归一化txt。YOLO格式每个txt文件对应一张图每行是class_id x_center y_center width height坐标全部除以图像宽高归一化到0到1之间。写转换函数时注意两点宽高用浮点除法类别id用int否则拼接字符串时类型会报错。def voc_to_yolo(ann, name_to_id, img_w, img_h): lines [] for obj in ann[objects]: cls_id name_to_id[obj[name]] xmin, ymin, xmax, ymax obj[bbox] x_center (xmin xmax) / 2.0 / img_w y_center (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) return lines坐标的分子是用最小最大值的平均求中心点分母是图像宽高。六个小数位足够YOLO训练使用太多位不会提升精度反而让txt文件变大。如果XML里的框越界转换后w或h可能出现负值或超出1所以这个函数要放在脏数据校验之后调用。4.2 脏数据清洗脚本和修复策略脏数据清洗是训练前最花时间的一步。可视化脚本能发现严重偏移结构性问题必须用脚本扫全量。通常检查四类XML里没有object、目标框越界、坐标顺序颠倒、图片读取失败。逐张遍历并汇总报告不中断处理先拿全貌再决定修法。from pathlib import Path import cv2 def validate_dataset(image_dir, annotation_dir): report {empty_xml: [], broken_image: [], out_of_bound: []} for xml_path in sorted(Path(annotation_dir).glob(*.xml)): ann parse_voc_xml(xml_path) if not ann[objects]: report[empty_xml].append(xml_path.name) continue img cv2.imread(str(Path(image_dir) / ann[filename])) if img is None: report[broken_image].append(xml_path.name) continue h, w img.shape[:2] for obj in ann[objects]: xmin, ymin, xmax, ymax obj[bbox] if xmin 0 or ymin 0 or xmax w or ymax h: report[out_of_bound].append( f{xml_path.name}: {obj[name]} {obj[bbox]} ) return reportvalidate_dataset返回三类问题列表。空XML在训练时不会直接报错但会让某些框架的collate函数在聚合batch时崩溃因为样本维度对不上。越界框转成YOLO格式后会出现xywh为负或大于1训练loss直接震荡。broken_image最常见原因是路径含空格、中文或软链接失效。检查完先看总数如果问题文件占比超过5%说明标注过程存在系统性错误需要回到工具链排查而不是一张张手动修。表脏数据类型与处理建议脏数据出现原因处理建议空XML标注中途中止另存为副本删除或人工补标框越界拖框超出画布边缘裁剪到边界保留面积大的部分坐标颠倒标注工具框选方向与约定相反交换xmin/xmax或ymin/ymax并加断言文件名不匹配重命名后XML未同步以XML的filename为准做全量核对类别拼写漂移不同标注员习惯不同按classes.json做归一化和映射提示清洗时把过滤结果写入filtered.txt记录不要直接改原XML副本放到Annotations_clean目录训练路径指向副本。每个删除动作都可追溯。5. 29类类别不平衡与硬样本抽检验证集指标之外的人工复核29类叶片病害的样本量大概率不均匀。常见病害可能有几千个框某些早期症状或稀有病害只有几十个框。类别不平衡会让模型对长尾类召回偏低而验证集mAP被常见类主导往往看不出问题。训练前先统计每个类别的框数和图片数在分布图上找出长尾类再决定要不要做针对性增强。5.1 类别分布统计与长尾类增强from collections import Counter def class_distribution(annotation_dir): box_counter Counter() image_counter Counter() for xml_path in sorted(annotation_dir.glob(*.xml)): ann parse_voc_xml(xml_path) obj_names [obj[name] for obj in ann[objects]] box_counter.update(obj_names) image_counter.update(set(obj_names)) return box_counter, image_counter dist, img_dist class_distribution(Path(Annotations)) for cls in sorted(dist.keys()): print(f{cls:30s} boxes{dist[cls]:5d} images{img_dist[cls]:5d})输出里如果某个类只有个位数框就要考虑针对性增强。对叶片病害来说最安全的增强是HSV颜色扰动、随机裁剪和马赛克这些不改变病斑的形态语义。旋转要谨慎部分病斑有方向性超过90度的旋转会生成现实中不存在的形态。长尾类的另一种思路是从同类里选相似的叶片做mixup但mixup后的框和标签边界会变模糊不如直接做在线增强直观。5.2 硬样本抽检的对比图生成训练完成后的验收阶段把验证集预测结果按置信度排序手工检查三类硬样本低置信度的漏检、高置信度的漏检、高置信度的误检。对应叶片病害常见现象是健康叶脉被当成病斑、两种叶斑病互相混淆、小尺寸病斑与背景融合。可视化脚本在这里复用把GT框和预测框画到同一张图实线表示真实标注虚线表示预测结果一眼能看出偏差。python visualize_compare.py \ --image test/leaf_0123.jpg \ --xml Annotations/leaf_0123.xml \ --pred results/leaf_0123.txt \ --classes classes.json \ --output compare_leaf_0123.jpg--pred文件每行是class_id score xmin ymin xmax ymax脚本先读classes.json把class id还原成类名再与GT框叠加绘制。框离得远的直接看偏移方向框基本重合但类别不同的看标签文字框缺失的说明漏检。拿30张错检图给植保专家或标注员复核他们的反馈比调增强参数更有指导性能直接指出哪几个类在视觉上难以区分下一步是合并类还是补样本就有了依据。把对比图按日期归档到hard_samples目录训练下一版模型时先重看这批图再决定改动方向。本文还有配套的精品资源点击获取