最近我把一套小白菜叶子病害健康状态检测数据集彻底整理了一遍——1863张图像、2个类别、VOC和YOLO两种标注格式都齐全。这套数据做的是叶片级目标检测,不是整图分类。也就是说,模型要输出的不是"这张图有病/没病",而是用边界框把每一片病害叶、每一片健康叶的具体位置标出来。对小白菜这类叶片密集、互相遮挡的叶菜来说,这个差异非常关键。
这套数据的价值在于:它同时提供了适合阅读和可视化的VOC标注,以及能直接喂给当前主流检测框架训练的YOLO标注,省去了转换格式这一大堆重复劳动。如果你在做植物表型分析、想跑通一个目标检测流程,或者刚入门CV需要一份能练手的农业数据集,这套数据是一个很舒服的起点。下文我会把数据格式、核验流程、训练适配和实际踩坑都详细过一遍,能帮你少走不少弯路。
1. 小白菜病害检测数据:这份1863张数据集的真实用途
1.1 叶片级目标检测与图像分类是两回事
一开始很多人会问:病害检测为什么要用目标检测,而不是直接做分类?原因很简单:大田和大棚里的白菜叶片从来不是单张干净地摆在画面中央,而是一株多叶、叶片交错、部分被遮挡,甚至一片叶子上既有病斑又有健康区域。分类模型只能给出"整图是否有病"的结论,但回答不了"病在哪里、有几处、面积多大"。
目标检测模型则要预测两件事:每个目标的位置(边界框)和类别(健康或病害)。以这套数据为例,一张图上可能有多个标注框,每个框对应一片叶子,模型要逐个识别。这个能力落到实际场景中才有用——比如自动喷药设备需要知道"病害叶集中在哪个区域",植保人员统计病情指数时也需要"发病叶片数量/总面积"这类数据,分类模型给不了这些。
正因为如此,数据处理的第一步就要明确:这是一份检测数据,不是分类数据。1863张图对应着近万级别的标注框,但标注的颗粒度是"框住叶片",而不是"框住病斑"。健康叶和病害叶都各有框,模型才学得会"对比着看"。
1.2 两类别设计是务实起点,不是偷懒
这套数据的2个类别,一般对应健康叶片和病害叶片。别觉得两类别少——实际上很多场景下,用户需要的恰恰只是一个可靠的"病害预警器",先拿它筛掉明显有问题的叶片,后续再人工复查或者细分病害类型。
在整理过一些农业数据之后,我越来越认同这种"先两分类,后细分类"的路线。原因有三个:第一,病害类型在田间往往混合出现,单张叶片可能同时有真菌性病斑和细菌性腐烂迹象,标注者很难干净地分成"这是霜霉病,那是软腐病",但归入"病害"类别是稳定一致的;第二,二分类模型的标注一致性更容易保证,多人协作标注时分歧小;第三,模型先解决"有没有病、在哪"的问题,比一上来就强行区分十几种病要稳得多。
这套数据适合做技术验证和流程打通。我用它跑过一次从数据检查到模型训练的全流程,熟练之后小半天就能出一个效果还不错的检测模型,作为算法预研或者课程项目完全没有问题。
2. VOC与YOLO双格式解析:同一批图像的两种标注表达
2.1 VOC格式:XML里的绝对坐标与可读性
VOC格式源自计算机视觉领域的一种公开数据集组织方式,核心是"图片文件夹 + XML标注文件夹 + 划分文件"。目录结构通常是:
VOC格式/ ├── JPEGImages/ # 存放所有图像 ├── Annotations/ # 存放所有XML标注文件 └── ImageSets/ └── Main/ # train.txt、val.txt 等划分文件每张图片对应一个同名XML文件,里面写清楚文件名、图像的宽高通道数,以及每个目标框的信息。看一个简化的例子:
<annotation> <filename>leaf_0001.jpg</filename> <size> <width>640</width> <height>640</height> <depth>3</depth> </size> <object> <name>healthy</name> <bndbox> <xmin>120</xmin> <ymin>80</ymin> <xmax>480</xmax> <ymax>420</ymax> </bndbox> </object> </annotation>注意这里的xmin/ymin/xmax/ymax是像素绝对坐标。哪个数字对应图像左上角、哪个对应右下角,必须和图像实际尺寸匹配,一旦图片被缩放或裁剪,坐标就没用了。VOC格式的最大优点是"人可读"——任何图像查看工具都能直接按XML把框画出来,想人工核查哪一张图都很方便。
2.2 YOLO格式:归一化坐标背后的训练逻辑
YOLO系列的训练格式则完全不同:每张图对应同名的.txt文件,每一行代表一个边界框,五个数字分别是类别索引 x_center y_center width height。这里有一个关键区别:后四个值全部是归一化坐标,范围在0到1之间。
0 0.468750 0.390625 0.562500 0.531250拿这一行举例:类别索引0(假设对应健康叶),框中心的x坐标是图像宽度的0.46875倍,中心y是高度的0.390625倍,框宽占整个图像的0.5625,框高占0.53125。归一化的好处是,训练时把输入分辨率从640改成1280,标注数字不用动,模型内部按比例换算即可。
两种格式的适用场景也完全不同,我做了个对比:
| 对比项 | VOC格式 | YOLO文本格式 |
|---|---|---|
| 坐标表达 | 像素绝对坐标 | 归一化相对坐标 |
| 可读性 | 好,适合核查 | 差,肉眼难判断含义 |
| 训练适配 | 需转换或借助工具 | 主流检测框架直接读取 |
| 文件单条信息量 | XML含图片尺寸等丰富信息 | 每行纯数值,简洁 |
| 常见问题 | 缩放图片后坐标失效 | 类别索引与配置顺序错位 |
2.3 两种格式转换的关键公式
很多情况下你会拿到其中一种格式,需要转成另一种。转换公式本身不难:
# VOC 像素坐标 -> YOLO 归一化坐标 img_w, img_h = 640, 640 x_center = ((xmin + xmax) / 2) / img_w y_center = ((ymin + ymax) / 2) / img_h box_w = (xmax - xmin) / img_w box_h = (ymax - ymin) / img_h反过来从YOLO转VOC时,用下面这组公式:
# YOLO 归一化坐标 -> VOC 像素坐标 xmin = int((x_center - box_w / 2) * img_w) ymin = int((y_center - box_h / 2) * img_h) xmax = int((x_center + box_w / 2) * img_w) ymax = int((y_center + box_h / 2) * img_h)这里最容易出问题的是边界:归一化坐标可能在极端情况下略微超出0~1,转回像素坐标后会出现负数或超过图像宽高的值。转换脚本里最好统一做一次clip截断,避免后面画框、训练报错。某次我处理另外一套数据时就遇到过xmax比图像宽度还大的情况,原因就是原始标注本身越界,但格式转换时没人检查。
3. 训练前的数据体检:1863张图像的核查链路
3.1 先对账:图片、XML、TXT必须一一对应
拿到数据集的第一件事不是开启训练,而是先"对账"。最简单有效的检查方式是写个脚本,把 JPEGImages 里的图片名、Annotations 里的XML名、YOLO格式下 labels 里的TXT名分别取出来,做差集,确认三套文件完全一致。
我平时会用一段很短的Python脚本来做这件事:
import os img_dir = "JPEGImages" xml_dir = "Annotations" label_dir = "labels" imgs = {os.path.splitext(f)[0] for f in os.listdir(img_dir) if f.endswith((".jpg", ".png"))} xmls = {os.path.splitext(f)[0] for f in os.listdir(xml_dir) if f.endswith(".xml")} txts = {os.path.splitext(f)[0] for f in os.listdir(label_dir) if f.endswith(".txt")} print("缺XML的图片:", len(imgs - xmls)) print("缺TXT的图片:", len(imgs - txts)) print("无对应图片的XML:", len(xmls - imgs)) print("无对应图片的TXT:", len(txts - imgs))只要差集不为空,就说明标注文件有缺失或多余。这个问题如果不在训练前发现,往往会在训练中途以FileNotFoundError或某张图没有任何标签的形式暴露出来,排查起来更费劲。
3.2 坐标与类别字段逐项核查
文件一一对应之后,还要逐条检查标注内容本身。拿我自己的核查经验来说,下面这些问题最容易出现,每一条都值得专门检查:
| 核查项 | 通过标准 | 失败时的典型表现 |
|---|---|---|
| 坐标边界 | 所有角点都在图像像素范围内 | 训练loss异常、画框越界 |
| 框尺寸 | 宽高均大于0,不至于过小 | 空标签告警、检测不到小目标 |
| 类别名称 | 大小写一致、无多余空格 | 类别数被错误加大 |
| 类别索引 | YOLO序号在0~1之间 | 配置names时对应错位 |
| 框与叶片贴合 | 人工观察后判断 | 模型学习背景特征 |
坐标和框尺寸可以用程序批量检查。类别名则建议直接打印所有XML里出现过的<name>值,以及所有TXT里的第一列取值,人为确认一下。一个常见坑是:同一片叶子在部分XML里写成healthy,另一部分写成Healthy,大小写不一致导致类别数量莫名变成3或4个。
3.3 画框可视化:脚本查不出来的人工环节
程序检查能解决80%的问题,但挡不住"标注框画歪了但坐标合法"这类情况。我的习惯是抽30到50张图,用OpenCV把边界框画在原图上,拼成网格图快速过一遍:
import cv2 image = cv2.imread("leaf_0001.jpg") # assume boxes are from XML or txt for (xmin, ymin, xmax, ymax, cls_id) in boxes: color = (0, 255, 0) if cls_id == 0 else (0, 0, 255) cv2.rectangle(image, (xmin, ymin), (xmax, ymax), color, 2) cv2.imwrite("check_0001.jpg", image)这一步看起来笨,但效率极高。很多脚本发现不了的问题,扫一眼图就能看出来:比如某张图里框只框住了叶片的一半、有几片叶子完全没标、或者病害叶的框把一大块泥土都包进去了。可视化复查应当覆盖不同发病程度的图,而不仅仅是前几张。
提示:可视化时建议把健康叶和病害叶用不同颜色画框,这样一眼就能看出类别是否标反。我见过部分数据里同一片叶子既被标了健康又被标了病害,这种矛盾样本一旦进入训练,会让模型在某片区域上反复摇摆。
4. 从数据到模型:双格式训练适配记录
4.1 数据划分的正确姿势
使用YOLO格式训练时,通常需要把数据分成images/train和images/val,有些场景还要单独拆出images/test。对1863张图像这个规模,我建议按8:1:1或者9:0.5:0.5的比例拆分。
两个容易踩的坑必须提醒:
第一,划分必须按图片来,不是按边界框来。如果你把同一张图上的不同框分到训练集和验证集,就等于让模型在验证时看到了训练目标的记忆碎片,评估指标会虚高。我的做法是:先给所有图片名随机打散,再按比例切分,整张图只进一个集合。
第二,如果数据采集自不同批次或不同环境,不要纯随机切分。宁可让其中一个环境整批进入验证集,也比把所有环境混合后随机切分更能说明泛化能力。随机切分的验证分数很漂亮,但一到新环境就崩,这种例子我见过太多次了。
4.2 数据集配置与训练参数
以当前主流的YOLO系检测框架为例,训练前需要准备一个数据集配置文件:
path: ./leaf_dataset train: images/train val: images/val nc: 2 names: 0: healthy 1: diseased类别名称这块必须和TXT里的索引顺序严格一致。如果原本的TXT里类别索引0是"diseased",配置文件里写明0是"healthy",模型会从头学到尾都学反了。
训练参数方面,我的基准配置是这样:
- 输入分辨率:640或更高,小白菜叶片小目标多,长边建议至少640
- 批量大小:根据显存调节,16或32
- 训练轮数:100轮起步,配合早停
- 预训练权重:加载COCO预训练权重,收敛速度快很多
- 数据增强:翻转、轻微HSV扰动、少量马赛克
4.3 评估指标与一次典型复盘
在1863张图、2类别的配置下,二分类检测模型通常能较快达到一个可用的精度。我比较看重三个指标:
| 指标 | 含义 | 观察要点 |
|---|---|---|
| Precision(精确率) | 预测为病害的框中真病害的比例 | 过高可能漏检,过低误检多 |
| Recall(召回率) | 真实病害框被找出的比例 | 偏低说明漏检严重 |
| mAP@0.5 | IoU阈值0.5下的平均精度 | 最常用的综合指标 |
某次验证性训练里,我的模型在验证集上mAP@0.5跑到了0.85左右,但mAP@0.5:0.95只有0.55上下。这个差距说明:模型能定位出叶片大致位置,但框的贴合度还有提升空间。如果你也遇到类似情况,不用急着认定是数据问题,先检查是不是叶片重叠导致框没法画得精准——这是这类数据的天然难点,不完全是模型能力问题。
5. 两类别叶片检测最容易踩的坑:实测记录与对策
5.1 背景抢戏与靠环境识病的假阳性
两类别数据集中,健康叶和病害叶如果在固定背景下拍摄,模型很容易学到一条"捷径":靠背景区分,而不是靠叶片本身。举个例子,如果所有病害叶照片都带一点土壤痕迹,而健康叶都干干净净,模型可能把任何带有土壤背景的区域都判成病害,进入新场景后立刻失效。
我验证这个问题的方法是:单独抽一批只有健康叶、背景换成不同颜色的图片,看模型会不会误报。会误报,就说明它确实在偷懒。对策有两个方面:一是标注时把框贴紧叶片边缘,减少背景进入框内;二是训练时用色彩抖动、随机背景替换之类的增强,强迫模型关注叶片纹理和颜色差异,而不是背景。
5.2 健康与病害样本失衡导致的偏置
数量上,健康叶往往比病害叶好采集得多,这会导致标注框数量不平衡。如果健康框是病害框的三倍以上,模型训练会偏向健康类,损失函数中病害类贡献占比相对小,最终表现为病害叶recall偏低。
最简单的处理方式是先统计两类框的数量:
import glob for txt_file in glob.glob("labels/*.txt"): for line in open(txt_file): cls_id = int(line.split()[0]) count[cls_id] += 1如果确实失衡,可以给少数类额外做几次复制并配合轻微变换,或者在训练配置中按类别频率设置损失权重。对二分类问题,我倾向于先做少量过采样,而不是动损失权重,因为过采样的行为更可控。
5.3 叶片重叠与小目标漏检
小白菜的叶片在俯拍视角下经常相互遮挡,一大片叶子遮住另一片,连人眼看图都时有犹豫。模型在这类场景下容易漏检被遮挡的叶片,尤其当病害叶边缘卷曲、颜色发黄,目标轮廓不清晰时。
小目标多的数据集,我建议做三件事:输入分辨率从640提到960;关注mAP@0.5:0.95而不是只看mAP@0.5;推理阶段适当降低置信度阈值,宁可多出几个误检再过滤,也不要让漏检占主导。如果用的是带马赛克增强的框架,还需要注意马赛克拼图会把小目标缩得更小,这时候要把马赛克概率调低,或者把训练前几轮的马赛克关掉,让模型先稳定一阵。
注意:病害检测严重依赖颜色和纹理特征。不要把HSV色相扰动调得太强,否则健康叶的绿色可能被增强成病态黄绿色,白白给模型制造错误样本。我的经验是色相扰动幅度控制在±10以内,饱和度和明度可以稍微放开一点。
6. 数据集的边界与下一步:从原型到真实田间的活用思路
6.1 这份数据不能覆盖的田间变化
这套数据集的图像大概率采集自相对可控的环境——比如大棚、实验田或者人工拍摄场景。真实田间部署时,光线方向、阴影、泥土飞溅、农药残留、相邻植株干扰、不同品种叶色差异,都会带来训练分布之外的输入。直接用这套数据训练出的模型下田,几乎必然出现性能下降。
这不代表数据集没有用,而是要用对方式。把它当作"基础训练集"而不是"最终数据源"更合适。先训练出具备基本检测能力的模型,再去目标地块少量采集新样本进行微调,是性价比最高的路径。
6.2 低成本扩展数据集的三种可行路径
如果后续要扩展这套数据,我实际验证过比较靠谱的方向有三个:
路径一:病害细分。在"病害"大类内部,把霜霉病、软腐病等常见叶菜问题逐步拆成独立类别,每类至少补到300到500个框,再做增量训练。
路径二:从检测走向分割。想让喷药或病情评估更精准,可以把叶片级标注细化为病斑级分割标注,检测框只能告诉你"这片叶子病了",分割才能告诉你说"病斑面积占叶面积的百分比"。
路径三:半监督自举。用现有模型给新场景的未标注图片打标签,人工只修正明显错误,再把修正后的数据并入训练集。这个方法能把人工标注量压缩到原来的三分之一左右,但要控制好置信度阈值,太低会产生大量错误伪标签。
这套数据在"小而精"这条路上做得不错:规模足够跑通管线,格式齐全不用二次处理,类别设定又避免了多病害混标的标注地狱。我个人的建议是,拿到数据后先不要急着跑训练,花一晚上把图片和标注完整过一遍,做好格式核验和分布统计,这个前置投入后面一定会从训练调试时间里省回来。如果你打算长期做植物检测方向,还可以顺手写一个通用的标注核查脚本,这套数据的双格式结构就是一个很好的练手样本。