简介:这是一份面向目标检测与植物保护领域研究者的植物叶片病害检测数据集,采用Pascal VOC标注格式,可直接用于训练YOLO、Faster R-CNN等常见检测模型,免去数据清洗与格式转换的额外工作。数据集覆盖多种植物叶片及霉菌、斑点、虫害等病害类型,每个XML文件记录对应的边界框位置与类别名称,标注信息细致完整,结构清晰,适合开展农业AI、智能植保等方向的模型训练与算法验证。资源共2000个文件,全部为XML标注文件,压缩包大小约386.67MB,数据按文件夹保存,下载解压后无需额外处理即可接入TensorFlow、PyTorch等主流深度学习框架。目前已有233人学习下载,适合具备一定深度学习基础、需要高质量标注数据完成病害检测项目的研究者或开发者使用,可直接用于模型训练、精度对比与算法优化。
1. 植物叶片病害检测数据集:VOC 格式的 2000 张图,拿到就能训
做目标检测,最耗时间的往往不是模型调参,而是标注数据的整理。这份植物叶片病害数据集共 2000 张图片和对应 XML 标注文件,按 Pascal VOC 格式组织,解压后不需要转格式、不用补字段,能直接进 YOLO 系列的目标检测训练流程。我做过几个农业视觉项目,公开数据里要么标注散乱,要么目录不统一、缺 size 字段,像这样规范保存的确实省掉大半预处理时间。适用人群很明确:正在做植物病害识别的算法工程师,想用现成数据跑通检测流程的学生,以及农业科技从业者。从样本文件名看,覆盖番茄黄化曲叶病毒、蓝莓、桃树、草莓、粉虱传播相关叶片,是多作物多病害的组合,练手、验证检测方案、做迁移学习底料都够用。
2. 数据集结构拆解:VOC 标注里有什么,训练前先看这三处
拿到数据集的第一件事不是急着解压跑训练,而是把标注格式、文件名规则、类别分布这三处底细摸清楚。VOC 格式看起来简单——一张图配一个 XML,但实际用起来,坐标字段理解偏一点,后面转换脚本和训练全得跟着错。这一章把 2000 张图的组织方式、XML 字段含义、以及怎么快速统计类别分布说清楚,这些步骤做扎实了,第 3 章的转换脚本才能一次跑通。
2.1 目录结构与文件名规则:先确认图与标注一一对应
数据集按文件夹保存,图片和 XML 标注文件分开存放。用之前先确认每张图都有同名 XML,并且 XML 里的 filename 字段和实际文件名一致,这两处对不上,训练时 dataloader 会随机报错,而且报错信息往往指向路径问题,排查起来绕远路。
文件名本身能读出不少信息。拿IU-TYLCV_img_6_jpg.rf.69e5513dca2698b0d02750f6720a9c22.xml来说,IU-TYLCV是来源批次标记,_img_6是原始图片编号,_jpg表示原始格式,中间那串.rf.加哈希是标注平台为避免合并数据时重名自动追加的唯一 ID。这种命名习惯在从标注平台导出的数据集里很常见,哈希后缀相当于全局唯一 ID,两个不同项目导出的图片原始文件名相同也不会互相覆盖。缺点是文件名变长,Windows 老版本对超长路径不友好,解压路径最好放浅一点,比如直接放D:\dataset,别套多级中文目录。
另外注意文件名里有blueberry-bushes-clipart-16这类明显带 clipart 字样的样本,说明数据源包含网络插图而非全部田间实拍。训练出的模型在真实农田照片上的泛化会打折扣,评估时最好按来源分组单独看指标,别被整体 mAP 迷惑。
先跑一遍数量对账,用 bash 脚本快速核实:
# 统计图片和 XML 数量 ls images/*.jpg | wc -l ls annotations/*.xml | wc -l # 找出缺少同名 XML 的图片 for img in images/*.jpg; do base=$(basename "$img" .jpg) [ ! -f "annotations/${base}.xml" ] && echo "缺少标注: $img" done # 找出没有对应图片的 XML for xml in annotations/*.xml; do base=$(basename "$xml" .xml) [ ! -f "images/${base}.jpg" ] && echo "缺少图片: $xml" done这段脚本先对比数量,再逐一对名。理论上 2000 张图就该有 2000 个 XML,数量对不上或者输出了缺失条目,先补齐再做转换。注意脚本里的 basename 去掉了扩展名,如果图片后缀不统一(比如混有 .png),for 循环里的匹配后缀要跟着改,否则同名判定会误报。
2.2 XML 字段拆解:bndbox 是绝对坐标,不是宽高
VOC 的核心约定是:一个 XML 描述一张图,图上每个目标对应一个 object 节点。下面是标准结构的示意,具体字段值以实际文件为准:
<annotation> <folder>images</folder> <filename>IU-TYLCV_img_6_jpg.rf.69e5513dca2698b0d02750f6720a9c22.jpg</filename> <size> <width>640</width> <height>640</height> <depth>3</depth> </size> <object> <name>leaf_disease</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>112</xmin> <ymin>88</ymin> <xmax>301</xmax> <ymax>372</ymax> </bndbox> </object> </annotation>训练前需要逐个确认的字段列成表更直观:
| 字段 | 含义 | 训练影响 |
|---|---|---|
| filename | 图片文件名 | 路径匹配靠它,与实际文件名不一致直接报错 |
| size/width、size/height | 图片宽高(像素) | 归一化坐标的分母,错了整个标注全部位移 |
| object/name | 类别名 | 决定类别映射表,一个名字写歪就多出一个类别 |
| bndbox/xmin、ymin、xmax、ymax | 左上角与右下角绝对坐标 | 必须是绝对像素坐标,不是「xmin + 宽高」 |
| truncated、difficult | 是否截断、是否难例 | difficult=1 的目标会被多数训练框架忽略 |
最容易踩的是 bndbox 的理解:VOC 里 xmax、ymax 是右下角在整张图上的绝对像素位置,不是相对 xmin 的偏移量。有的标注工具导出时会写成宽高格式,转换脚本按宽高解析,框就会整体错位。叶片病害场景里一张图往往有多个病斑,一个 XML 里有多个 object 节点,转换脚本必须循环处理,不能只取第一个。这份数据集声明是标准 VOC,理论上不会出现上述问题,但我的习惯是转换前随机抽三五张,用可视化工具核对 bndbox 和病斑位置,成本很低,却能避免整批框错位的连锁问题。
2.3 类别统计:决定映射表和训练策略的一步
转换脚本里类别表怎么排、要不要合并相似类别,都取决于 XML 里实际出现了哪些 name。先用脚本统计一遍:
import glob import xml.etree.ElementTree as ET from collections import Counter counter = Counter() total_boxes = 0 empty_files = [] for xml_file in glob.glob('annotations/*.xml'): tree = ET.parse(xml_file) root = tree.getroot() objs = root.findall('object') if not objs: empty_files.append(xml_file) for obj in objs: name = obj.find('name').text if name: counter[name] += 1 total_boxes += 1 print('类别分布:', dict(counter)) print('总框数:', total_boxes) print('空标注文件数:', len(empty_files)) print('空标注文件列表:', empty_files[:10])这段脚本遍历所有 XML,对每个 object 节点取 name 计数,同时记录有没有一张图一个框都没有的情况。输出结果后重点关注两件事:一是空标注文件多不多,空了 10 张以上说明有漏标,训练时这些图会被当成纯背景,影响不算大但会拉低召回;二是类别分布是否均衡,某个病害类别只有几十个框、别的类别上千时,小类很容易被大类淹没,处理办法在第 4 章。注意如果 XML 里 name 字段缺失或为空,程序不会报错但计数会少,这类文件也会在空标注检查里暴露出来。统计完顺手把分布存成 csv,后面写实验记录和论文都要用。
提示:统计出的类别名直接决定第 3 章的 CLASS_NAMES 顺序,先在这里统一好名字,别到转换脚本里再改。
3. 从 VOC 转到 YOLO:转换脚本、数据集划分与训练前校验
YOLO 系列训练不认 XML,只认每张图一个 txt、每行「类别id 中心x 中心y 宽 高」的格式,坐标全部归一化到 0~1。所以拿到 VOC 数据后的标准动作是:先转格式,再划分数据集,最后跑校验确认没有脏数据。随资源一起提供的 YOLO 实战教程和 YOLOv5 改进实战系列就是围绕这条链路讲的,卡在某个环节时对照着看比盲调参数快。
3.1 VOC 转 YOLO 脚本:归一化计算与边界钳制
转换公式本身不复杂:把 bndbox 的四个绝对坐标换算成中心点坐标和宽高,再分别除以图片宽高得到归一化值。真正容易出问题的是边界——有的标注框会超出图片尺寸几个像素,或者某条 XML 的 size 字段和实际图片不一致,这些脏数据不处理,转换出来的坐标可能大于 1 或是负数,训练时 loss 直接崩。所以脚本里我做两件事:坐标钳制和无效框过滤。
转换后的 txt 每行对应一个目标,行顺序和 XML 里的 object 顺序保持一致即可,YOLO 训练时不关心 txt 内部顺序。文件命名必须和图片完全同名(只差扩展名),这是 YOLO 系列定位标签的唯一方式,名字对不上就等于没有标注。
import xml.etree.ElementTree as ET from pathlib import Path # 类别表:先按第 2.3 节统计结果填好,顺序固定后不要改 CLASS_NAMES = ['healthy', 'tylcv', 'leaf_spot', 'mildew', 'pest_damage'] def clamp(value, low, high): return max(low, min(value, high)) def voc_to_yolo(xml_path: Path, out_txt: Path) -> int: tree = ET.parse(xml_path) root = tree.getroot() # size 字段是归一化的分母,缺失或为 0 直接跳过该文件 size = root.find('size') img_w = int(size.find('width').text) img_h = int(size.find('height').text) if img_w <= 0 or img_h <= 0: print(f'无效尺寸,跳过: {xml_path.name}') return 0 lines = [] for obj in root.findall('object'): name = obj.find('name').text if name not in CLASS_NAMES: print(f'未知类别 {name} 于 {xml_path.name}') continue cls_id = CLASS_NAMES.index(name) box = obj.find('bndbox') xmin = clamp(float(box.find('xmin').text), 0, img_w - 1) ymin = clamp(float(box.find('ymin').text), 0, img_h - 1) xmax = clamp(float(box.find('xmax').text), 0, img_w - 1) ymax = clamp(float(box.find('ymax').text), 0, img_h - 1) # 钳制后变成零宽或零高,说明原始框有问题,过滤掉 if xmax <= xmin or ymax <= ymin: print(f'无效框,跳过: {xml_path.name} {name}') continue # YOLO 格式:类别id + 归一化中心点 + 归一化宽高 x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h width = (xmax - xmin) / img_w height = (ymax - ymin) / img_h # 二次钳制到 [0, 1],防止浮点误差溢出 x_center = clamp(x_center, 0.0, 1.0) y_center = clamp(y_center, 0.0, 1.0) width = clamp(width, 0.0, 1.0) height = clamp(height, 0.0, 1.0) lines.append(f'{cls_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}') out_txt.write_text('\n'.join(lines) + '\n', encoding='utf-8') return len(lines) # 批量转换 xml_dir = Path('annotations') out_dir = Path('labels') out_dir.mkdir(exist_ok=True) total_boxes = 0 for xml_file in sorted(xml_dir.glob('*.xml')): out_txt = out_dir / (xml_file.stem + '.txt') total_boxes += voc_to_yolo(xml_file, out_txt) print(f'转换完成,共写入 {total_boxes} 个框')脚本逻辑是:解析每个 XML,取 size 做归一化分母;遍历所有 object,把类别名映射成 id,bndbox 四个坐标钳制到图片范围内,再换算成 YOLO 需要的中心点和宽高并归一化,最后逐行写入同名 txt。需要按实际数据调整的只有 CLASS_NAMES,顺序就是训练时的类别 id,0 对应第一类,一旦开始训练就不要再改。
遇到未知类别我选择打印警告并跳过,而不是报错终止。2000 张图里偶尔一两张标注不干净是常态,跳过脏样本比中断整个流程划算,代价是那几个框不参与训练。想严格一点就把 continue 改成抛异常停住人工处理,两种策略按容错偏好选。
3.2 数据集划分:固定随机种子,按比例拆 train 和 val
转换完之后按 YOLO 惯例组织成下面的目录结构,YOLOv5 和 YOLOv8 的数据配置都能直接用:
dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/这个结构是 YOLOv5、YOLOv8 里 data yaml 最常见的组织方式,images 和 labels 的 train、val 子目录一一对应。划分要解决两个问题:一是图片和 txt 必须成对移动,不能只挪一边;二是随机划分要固定种子,保证每次跑出来的训练集、验证集完全一致,对比实验才有意义。
import random import shutil from pathlib import Path def split_and_copy(images, src_lbl_dir, img_train_dir, img_val_dir, lbl_train_dir, lbl_val_dir, val_ratio=0.2, seed=42): random.seed(seed) shuffled = images[:] random.shuffle(shuffled) val_n = int(len(shuffled) * val_ratio) val_files = shuffled[:val_n] train_files = shuffled[val_n:] for d in (img_train_dir, img_val_dir, lbl_train_dir, lbl_val_dir): d.mkdir(parents=True, exist_ok=True) for img in train_files: shutil.copy2(img, img_train_dir / img.name) lbl = src_lbl_dir / (img.stem + '.txt') if lbl.exists(): shutil.copy2(lbl, lbl_train_dir / lbl.name) for img in val_files: shutil.copy2(img, img_val_dir / img.name) lbl = src_lbl_dir / (img.stem + '.txt') if lbl.exists(): shutil.copy2(lbl, lbl_val_dir / lbl.name) return len(train_files), len(val_files) img_dir = Path('images') lbl_dir = Path('labels') images = sorted(img_dir.glob('*.jpg')) train_n, val_n = split_and_copy( images, lbl_dir, Path('dataset/images/train'), Path('dataset/images/val'), Path('dataset/labels/train'), Path('dataset/labels/val'), val_ratio=0.2, seed=42, ) print(f'训练集 {train_n} 张,验证集 {val_n} 张')核心是random.seed(42)这一行:种子固定,shuffle 结果就可复现,A 机器上划分的结果和 B 机器完全一样。val_ratio 取 0.2,2000 张里分出约 400 张做验证,对这类量级是常用比例。如果类别分布严重不均衡,建议改成按类别分层抽样,保证每个类在训练集和验证集的比例一致,否则某些病害类别可能全落进验证集,训练时一个样本都见不到。
注意:划分用的是 copy 而不是 move,原图保留一份在 images 目录,改坏数据集时还能回退,算是一份后悔药。
3.3 训练前校验:越界、倒挂、缺失一次查清
转换和划分完成后,训练启动前再跑一个独立校验脚本,把四类脏数据一次查清:坐标越界、坐标倒挂(xmin >= xmax)、size 与图实际尺寸不符、XML 与图片文件不对应。这一步让错误在训练前暴露,而不是等 loss 飘了再回头找原因。
import xml.etree.ElementTree as ET from pathlib import Path from PIL import Image def validate_annotations(xml_dir: Path, img_dir: Path) -> list: problems = [] for xml_file in sorted(xml_dir.glob('*.xml')): tree = ET.parse(xml_file) root = tree.getroot() filename = root.find('filename').text img_path = img_dir / filename if not img_path.exists(): problems.append(f'图片缺失: {filename}') continue # 用实际图片尺寸和 XML 里的 size 字段对比 with Image.open(img_path) as im: real_w, real_h = im.size size = root.find('size') xml_w = int(size.find('width').text) xml_h = int(size.find('height').text) if (xml_w, xml_h) != (real_w, real_h): problems.append(f'size 不一致: {filename} XML=({xml_w},{xml_h}) 实际=({real_w},{real_h})') for obj in root.findall('object'): name = obj.find('name').text 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 > real_w or ymax > real_h: problems.append(f'坐标越界: {filename} {name}') if xmin >= xmax or ymin >= ymax: problems.append(f'坐标倒挂: {filename} {name}') return problems problems = validate_annotations(Path('annotations'), Path('images')) if problems: print(f'发现 {len(problems)} 个问题:') for p in problems[:30]: print(' ', p) else: print('校验通过,可以开始训练')这段脚本里最有价值的是 size 一致性比对:很多数据集预处理时 resize 过图片,但忘了同步改 XML 的 size 字段,转换脚本拿错的宽高做分母,所有归一化坐标集体偏移,模型在训练集上看着没问题,换到真实场景框全歪。用 PIL 读实际尺寸对比,能提前暴露这类问题。脚本只打印前 30 条问题是为了避免刷屏,全量问题建议重定向到文件:python validate.py > issues.txt,修完一条删一条,最后一轮校验输出为空再进训练。这个流程看着多一道手续,实际每次都能拦下几个脏样本,值回时间。
4. 目标检测训练避坑:四类高发问题的现象、原因与解法
2000 张图的数据量不算大,跑一轮训练很快,但快的前提是数据没坑。这一章把实际踩过的四类高发问题整理成「现象 → 原因 → 解决」的记录,前两类和标注数据直接相关,后两类在植物叶片这种小目标密集的场景里尤其常见。
4.1 训练中 Loss 变成 NaN,模型直接废掉
现象:训练到十几个 epoch,loss 突然变成 nan,之后不管怎么降学习率都救不回来,只能重新跑。
原因:数据里存在越界标注。转换脚本如果没有钳制坐标,某几个框的归一化宽高大于 1 或者出现负值,模型在 anchor 匹配和损失计算时拿到非法数值,梯度爆炸后 loss 一路飘成 nan。最隐蔽的情况是某张图的 size 字段写的是 640,实际图片是 1280,转换出来的坐标整体缩小一半,虽然不炸 nan,但那个框对应的目标永远学不对。
解决:转换脚本按第 3.1 节的写法做坐标钳制,训练前跑第 3.3 节的校验脚本。如果训练已经炸了,先把校验脚本输出的越界 XML 找出来删掉或修正,再重新转换、重新训练,别试图在训练中途改数据,之前的权重已经污染了。判断是不是越界导致的 nan 有个小技巧:看 loss 曲线在第几个 epoch 炸的,再回校验脚本输出里找对应图片;如果集中在某个类别,优先查该类别的框。
4.2 类别名不一致,mAP 一直在 0 附近打转
现象:训练流程全正常,loss 稳定下降,但验证时 mAP 长期在 0 附近,或者预测出的类别张冠李戴。
原因:XML 里同类病害存在多种写法。比如同一种病毒,有的文件写TYLCV,有的写Tomato_Yellow_Leaf_Curl_Virus,统计脚本跑出来七八个类,你以为训练 5 类,实际模型在学 7 类,其中几个类只有几十个样本,根本学不出来。另一种情况是 name 带空格或大小写不一致,leaf_spot和Leaf_Spot被当成两个类。
解决:回到 2.3 节的统计结果,把同义写法统一成一份类别表。常见做法是维护一个别名映射,转换脚本先过别名再映射 id。统一的类别表存成文件放进项目目录,后面加数据、换人接手都按这份表来,就不会再出现第二遍同样的坑。
# 别名归一化示例:统一后再映射 id ALIAS = { 'TYLCV': 'tylcv', 'Tomato_Yellow_Leaf_Curl_Virus': 'tylcv', 'leaf_spot': 'leaf_spot', 'Leaf_Spot': 'leaf_spot', 'mildew': 'mildew', 'Mildew': 'mildew', } name = obj.find('name').text.strip() name = ALIAS.get(name, name) # 没命中的保留原值 if name not in CLASS_NAMES: print(f'未知类别 {name}') continue这段处理放在类别映射之前,先做字符串清洗(去首尾空格),再做别名归一化,最后查类别表,顺序不能反——带空格的名称直接查表必然 miss。改完重新统计类别分布,确认类别数和预期一致再进训练。另外训练配置里 data yaml 的 names 列表长度和顺序必须和 CLASS_NAMES 完全一致,长度对不上时训练器会静默忽略一部分类别,表现同样是 mAP 异常。我见过有人改了 CLASS_NAMES 忘了改 yaml,训练日志里类别数和预想差一个,排查了半天。
4.3 小目标漏检:叶片病斑小、特征弱,怎么调都不行
现象:模型对大叶片上的病害能检出,但早期小病斑漏检严重,IoU 0.5 的 mAP 还能看,换 IoU 0.75 直接腰斩。
原因:叶片病害的小病斑经常只有几十个像素,在 YOLO 的深层特征图(stride 32)上占很小区域,下采样后特征基本丢失;另一个因素是正样本框太小,匹配到的 anchor 数少,模型学不到足够的正样本特征。
解决:按性价比从高到低试这几招。第一招,输入分辨率从 640 提到 1280,小目标占的像素数翻倍,收益最明显,代价是显存占用涨。第二招,小目标过采样,把含小病斑的图复制几份放进训练集,变相提高正样本占比。第三招,检查 anchor 设置,YOLOv5 里在 yaml 里配更小的 anchor,YOLOv8 是 anchor-free 结构,主要靠提升分辨率和过采样。辅助手段是开启 YOLOv5 的--multi-scale,训练时输入尺寸在 0.5~1.5 倍随机缩放,等于变相对小目标做增强,缺点是训练时间变长、batch 尺寸不固定,显存规划要留余量。另外验证指标要按小目标分开看,COCO 风格 mAP 按面积分档,小目标指标不涨说明问题在小目标本身。具体的 anchor 修改位置和 YOLOv5 改进做法,随资源提供的实战教程里有分步说明,对照着改比盲调快。
4.4 数据增强把标注框带歪,训练集 mAP 虚高
现象:训练集 mAP 很高,验证集明显更低,差距大得不正常;或者开启 mosaic、旋转增强后,肉眼检查增强图,框和病斑明显错位。
原因:增强模块只对图像做变换,没有同步变换标注框。最常见的是自己写数据集类时手动加了 OpenCV 的随机旋转、缩放,图像变了但 bbox 坐标没跟着算;或者用 mosaic 拼接时贴图位置和标签对不上。
解决:优先用训练框架自带的增强,不要自己拼。YOLOv5 和 YOLOv8 的 hyp 配置里内置了 mosaic、hsv 扰动、随机翻转、缩放,这些增强都会同步处理标签。特别注意翻转类增强:水平翻转后 xmin 和 xmax 要交换,旋转 90 度后宽高互换,自己写很容易漏。检查增强是否正确,最简单是训练时开着可视化开关定期保存增强图,头几个 epoch 检查一次,标签错位一眼就能看出来。如果发现增强图里标签已错位,优先怀疑自己写的增强逻辑,而不是框架。
注意:分辨率提升能解决小目标,但会和显存打架。1280 分辨率加 batch 16 在 8G 显存上一般跑不动,batch 降到 8 或者用梯度累积,别硬调。
5. 把这份数据用满:半自动标注回补与病害严重度量化
2000 张标注图训练出一个能用的模型只是第一步,农业场景里真正有价值的是两件事:一是用模型去处理没标注的新数据,二是把检测结果转成农学意义上的指标。这里讲两个我常用的落地技巧。
5.1 用训练好的模型半自动标注回补新样本
数据不够的时候,与其人工从零标注,不如让训练好的模型先打一遍底稿。做法是:用 best.pt 对未标注图片推理,置信度超过阈值的检测框直接写成 YOLO 格式标签,低于阈值的部分留给人手工补。
import torch # 加载训练好的模型,pandas 接口需要先 pip install pandas openpyxl model = torch.hub.load('ultralytics/yolov5', 'custom', path='best.pt', force_reload=True) results = model('unlabeled/field_001.jpg', size=640) df = results.pandas().xyxy[0] # 列:xmin ymin xmax ymax confidence class # 只保留高置信度框,写回 YOLO 格式标签 high_conf = df[df['confidence'] >= 0.85] with open('unlabeled/field_001.txt', 'w', encoding='utf-8') as f: for _, row in high_conf.iterrows(): img_h, img_w = results.ims[0].shape[0], results.ims[0].shape[1] x_center = (row['xmin'] + row['xmax']) / 2.0 / img_w y_center = (row['ymin'] + row['ymax']) / 2.0 / img_h width = (row['xmax'] - row['xmin']) / img_w height = (row['ymax'] - row['ymin']) / img_h f.write(f"{int(row['class'])} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}\n")置信度阈值 0.85 是经验值,宁可少写、不要写错,错误标签混进训练集比漏标危害更大。回补后的新样本单独建目录和原始标注分开存放,实验时先只混入高置信度部分验证效果。低置信度的框不要删,存成 candidates 列表,之后用 labelImg 打开只做修正,比从零画框快得多。
5.2 从检测框到病害严重度评估
检测框只能告诉你「哪里有病害」,农学上更关心「有多严重」。最粗的做法是算框占叶片面积的比例,但矩形框会把健康区域也算进去,误差偏大。我一般用检测框做 ROI,在 ROI 内做颜色阈值分割,统计病斑像素占比,比单纯框面积准不少。
import cv2 import numpy as np def lesion_ratio(image_bgr, box): x1, y1, x2, y2 = [int(v) for v in box] roi = image_bgr[y1:y2, x1:x2] hsv = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV) # 黄化、褐变病斑在 HSV 空间的大致范围,数值按实际病害调整 mask = cv2.inRange(hsv, (15, 40, 40), (40, 255, 255)) diseased = np.count_nonzero(mask) total = roi.shape[0] * roi.shape[1] return diseased / total if total > 0 else 0.0 # 使用示例:df 来自上一节的 results.pandas().xyxy[0] img = cv2.imread('unlabeled/field_001.jpg') for _, row in df.iterrows(): ratio = lesion_ratio(img, (row['xmin'], row['ymin'], row['xmax'], row['ymax'])) print(f"病害类别 {int(row['class'])} 严重度 {ratio:.2%}")HSV 阈值依赖具体病害颜色,黄化类病害用 (15, 40, 40) 到 (40, 255, 255) 通常能覆盖,褐斑、霉层需要重新标定。我一般会在每张图上叠加 mask 可视化检查一遍,确认没有把绿叶健康组织算进去。这个思路对农业保险评估、田间长期监测够用,想要更精细就换分割模型,但成本和标注量都会上一个台阶。如果你也在找一份能直接开工的植物叶片病害检测数据,这份 VOC 格式的 2000 张图值得按上面的流程完整跑一遍。从那以后我每次拿到新数据集,都强制先跑类别统计和坐标校验再谈训练,转换脚本里永远留着钳制逻辑,哪怕当前数据没问题,也不赌下一批不会脏。这套习惯防住过太多次莫名其妙的翻车,希望帮到你。
本文还有配套的精品资源,点击获取